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