001package ca.uhn.fhir.jpa.interceptor.validation;
002
003/*-
004 * #%L
005 * HAPI FHIR Storage api
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.context.FhirContext;
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.exceptions.PreconditionFailedException;
030import ca.uhn.fhir.util.ExtensionUtil;
031import ca.uhn.fhir.util.OperationOutcomeUtil;
032import com.google.common.collect.ArrayListMultimap;
033import com.google.common.collect.Multimap;
034import org.apache.commons.lang3.Validate;
035import org.hl7.fhir.instance.model.api.IBaseResource;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038
039import javax.annotation.Nonnull;
040import java.util.Collection;
041import java.util.List;
042import java.util.stream.Collectors;
043
044import static ca.uhn.fhir.util.HapiExtensions.EXT_RESOURCE_PLACEHOLDER;
045
046/**
047 * This interceptor enforces validation rules on any data saved in a HAPI FHIR JPA repository.
048 * See <a href="https://hapifhir.io/hapi-fhir/docs/validation/repository_validating_interceptor.html">Repository Validating Interceptor</a>
049 * in the HAPI FHIR documentation for more information on how to use this.
050 */
051@Interceptor
052public class RepositoryValidatingInterceptor {
053
054        private static final Logger ourLog = LoggerFactory.getLogger(RepositoryValidatingInterceptor.class);
055        private final Multimap<String, IRepositoryValidatingRule> myRules = ArrayListMultimap.create();
056        private FhirContext myFhirContext;
057
058        /**
059         * Constructor
060         * <p>
061         * If this constructor is used, {@link #setFhirContext(FhirContext)} and {@link #setRules(List)} must be called
062         * manually before the interceptor is used.
063         */
064        public RepositoryValidatingInterceptor() {
065                super();
066        }
067
068        /**
069         * Constructor
070         *
071         * @param theFhirContext The FHIR Context (must not be <code>null</code>)
072         * @param theRules       The rule list (must not be <code>null</code>)
073         */
074        public RepositoryValidatingInterceptor(FhirContext theFhirContext, List<IRepositoryValidatingRule> theRules) {
075                setFhirContext(theFhirContext);
076                setRules(theRules);
077        }
078
079        /**
080         * Provide the FHIR Context (mandatory)
081         */
082        public void setFhirContext(FhirContext theFhirContext) {
083                myFhirContext = theFhirContext;
084        }
085
086        /**
087         * Provide the rules to use for validation (mandatory)
088         */
089        public void setRules(List<IRepositoryValidatingRule> theRules) {
090                Validate.notNull(theRules, "theRules must not be null");
091                myRules.clear();
092                for (IRepositoryValidatingRule next : theRules) {
093                        myRules.put(next.getResourceType(), next);
094                }
095
096                String rulesDescription = "RepositoryValidatingInterceptor has rules:\n" + describeRules();
097                ourLog.info(rulesDescription);
098
099        }
100
101        /**
102         * Returns a multiline string describing the rules in place for this interceptor.
103         * This is mostly intended for troubleshooting, and the format returned is only
104         * semi-human-consumable.
105         */
106        @Nonnull
107        public String describeRules() {
108                return " * " + myRules
109                        .values()
110                        .stream()
111                        .distinct()
112                        .map(t -> t.toString())
113                        .sorted()
114                        .collect(Collectors.joining("\n * "));
115        }
116
117        /**
118         * Interceptor hook method. This method should not be called directly.
119         */
120        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
121        void create(RequestDetails theRequestDetails, IBaseResource theResource) {
122                handle(theRequestDetails, theResource);
123        }
124
125        /**
126         * Interceptor hook method. This method should not be called directly.
127         */
128        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
129        void update(RequestDetails theRequestDetails, IBaseResource theOldResource, IBaseResource theNewResource) {
130                handle(theRequestDetails, theNewResource);
131        }
132
133        private void handle(RequestDetails theRequestDetails, IBaseResource theNewResource) {
134                
135                Validate.notNull(myFhirContext, "No FhirContext has been set for this interceptor of type: %s", getClass());
136                if (!isPlaceholderResource(theNewResource)) {
137                        String resourceType = myFhirContext.getResourceType(theNewResource);
138                        Collection<IRepositoryValidatingRule> rules = myRules.get(resourceType);
139                        for (IRepositoryValidatingRule nextRule : rules) {
140                                IRepositoryValidatingRule.RuleEvaluation outcome = nextRule.evaluate(theRequestDetails, theNewResource);
141                                if (!outcome.isPasses()) {
142                                        handleFailure(outcome);
143                                }
144                        }
145                } 
146        }
147
148        /**
149         * Return true if the given resource is a placeholder resource, as identified by a specific extension
150         * @param theNewResource the {@link IBaseResource} to check
151         * @return whether or not this resource is a placeholder.
152         */
153        private boolean isPlaceholderResource(IBaseResource theNewResource) {
154                return ExtensionUtil.hasExtension(theNewResource, EXT_RESOURCE_PLACEHOLDER);
155        }
156
157        protected void handleFailure(IRepositoryValidatingRule.RuleEvaluation theOutcome) {
158                if (theOutcome.getOperationOutcome() != null) {
159                        String firstIssue = OperationOutcomeUtil.getFirstIssueDetails(myFhirContext, theOutcome.getOperationOutcome());
160                        throw new PreconditionFailedException(Msg.code(574) + firstIssue, theOutcome.getOperationOutcome());
161                }
162                throw new PreconditionFailedException(Msg.code(575) + theOutcome.getFailureDescription());
163        }
164
165}