
001package org.hl7.fhir.common.hapi.validation.support; 002 003import ca.uhn.fhir.context.FhirContext; 004import ca.uhn.fhir.context.support.IValidationSupport; 005import ca.uhn.fhir.context.support.ValidationSupportContext; 006import ca.uhn.fhir.i18n.Msg; 007import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 008import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 009import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 010import ca.uhn.fhir.util.Logs; 011import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; 012import com.google.common.annotations.VisibleForTesting; 013import org.apache.commons.lang3.Validate; 014import org.hl7.fhir.common.hapi.validation.validator.ProfileKnowledgeWorkerR5; 015import org.hl7.fhir.common.hapi.validation.validator.WorkerContextValidationSupportAdapter; 016import org.hl7.fhir.instance.model.api.IBaseResource; 017import org.hl7.fhir.r5.conformance.profile.ProfileKnowledgeProvider; 018import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; 019import org.hl7.fhir.r5.context.IWorkerContext; 020import org.hl7.fhir.r5.model.StructureDefinition; 021import org.hl7.fhir.utilities.validation.ValidationMessage; 022import org.slf4j.Logger; 023 024import java.util.ArrayList; 025 026import static org.apache.commons.lang3.StringUtils.isBlank; 027 028/** 029 * Simple validation support module that handles profile snapshot generation. 030 * <p> 031 * This module currently supports the following FHIR versions: 032 * <ul> 033 * <li>DSTU3</li> 034 * <li>R4</li> 035 * <li>R5</li> 036 * </ul> 037 */ 038public class SnapshotGeneratingValidationSupport implements IValidationSupport { 039 @VisibleForTesting 040 public static final String GENERATING_SNAPSHOT_LOG_MSG = "Generating snapshot for StructureDefinition: {}"; 041 042 public static final String CURRENTLY_GENERATING_USERDATA_KEY = 043 SnapshotGeneratingValidationSupport.class.getName() + "_CURRENTLY_GENERATING"; 044 private static final Logger ourLog = Logs.getTerminologyTroubleshootingLog(); 045 private final FhirContext myCtx; 046 private final VersionCanonicalizer myVersionCanonicalizer; 047 private final IWorkerContext myWorkerContext; 048 049 /** 050 * Constructor 051 */ 052 public SnapshotGeneratingValidationSupport(FhirContext theFhirContext) { 053 this(theFhirContext, null); 054 } 055 056 /** 057 * Constructor 058 */ 059 public SnapshotGeneratingValidationSupport(FhirContext theFhirContext, IWorkerContext theWorkerContext) { 060 Validate.notNull(theFhirContext, "theFhirContext must not be null"); 061 myCtx = theFhirContext; 062 myVersionCanonicalizer = new VersionCanonicalizer(theFhirContext); 063 myWorkerContext = theWorkerContext; 064 } 065 066 @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") 067 @Override 068 public IBaseResource generateSnapshot( 069 ValidationSupportContext theValidationSupportContext, 070 IBaseResource theInput, 071 String theUrl, 072 String theWebUrl, 073 String theProfileName) { 074 075 /* 076 * We synchronize on the StructureDefinition instance because there is always 077 * the possibility that multiple threads are trying to generate a snapshot 078 * on the given resource at the same time. This could happen for example if 079 * multiple threads are validating resources against the same profile/StructureDef 080 * which is stored without a snapshot. We need to synchronize this because 081 * we read and write the UserData map in the resource, and this isn't thread 082 * safe. 083 * 084 * There shouldn't be any meaningful performance impacts to this synchronization 085 * because we cache the results of snapshot generation after we're done, so this 086 * lock is only hit during the first attempt by the validator to fetch 087 * any given SD (and any other concurrent attempts at the same time, hence 088 * needing this lock) 089 */ 090 synchronized (theInput) { 091 092 /* 093 * In cases of circular dependencies between StructureDefinitions, we can 094 * end up in a recursive loop. We use a userData variable in the 095 * StructureDefinition to flag and detect this situation. 096 */ 097 if (theInput.getUserData(CURRENTLY_GENERATING_USERDATA_KEY) != null) { 098 String url = myCtx.newTerser().getSinglePrimitiveValueOrNull(theInput, "url"); 099 ourLog.info("Detected circular dependency, already generating snapshot for: {}", url); 100 return theInput; 101 } 102 103 try { 104 theInput.setUserData(CURRENTLY_GENERATING_USERDATA_KEY, CURRENTLY_GENERATING_USERDATA_KEY); 105 106 /* 107 * We clone the resource that we're generating a snapshot for because 108 * the ProfileUtilities snapshot generator modifies the SD that gets 109 * passed in, and there is no guarantee that other threads aren't 110 * looking at it or even trying to iterate through the existing 111 * snapshot (if there is one) at the time we do this. 112 */ 113 IBaseResource inputClone = myCtx.newTerser().clone(theInput); 114 org.hl7.fhir.r5.model.StructureDefinition inputCanonical = 115 myVersionCanonicalizer.structureDefinitionToCanonical(inputClone); 116 117 String baseDefinition = inputCanonical.getBaseDefinition(); 118 if (isBlank(baseDefinition)) { 119 throw new PreconditionFailedException(Msg.code(704) + "StructureDefinition[id=" 120 + inputCanonical.getIdElement().getId() + ", url=" + inputCanonical.getUrl() 121 + "] has no base"); 122 } 123 124 IWorkerContext workerContext = myWorkerContext; 125 if (workerContext == null) { 126 workerContext = new WorkerContextValidationSupportAdapter( 127 theValidationSupportContext.getRootValidationSupport()); 128 } 129 130 StructureDefinition base = workerContext.fetchResource(StructureDefinition.class, baseDefinition); 131 if (base == null) { 132 throw new PreconditionFailedException(Msg.code(705) + "Unknown base definition: " + baseDefinition); 133 } 134 135 ArrayList<ValidationMessage> messages = new ArrayList<>(); 136 ProfileKnowledgeProvider profileKnowledgeProvider = new ProfileKnowledgeWorkerR5(myCtx); 137 ProfileUtilities profileUtilities = 138 new ProfileUtilities(workerContext, messages, profileKnowledgeProvider); 139 140 ourLog.info(GENERATING_SNAPSHOT_LOG_MSG, inputCanonical.getUrl()); 141 profileUtilities.generateSnapshot(base, inputCanonical, theUrl, theWebUrl, theProfileName); 142 143 return myVersionCanonicalizer.structureDefinitionFromCanonical(inputCanonical); 144 145 } catch (BaseServerResponseException e) { 146 throw e; 147 } catch (Exception e) { 148 throw new InternalErrorException(Msg.code(707) + "Failed to generate snapshot", e); 149 } finally { 150 theInput.setUserData(CURRENTLY_GENERATING_USERDATA_KEY, null); 151 } 152 } 153 } 154 155 @Override 156 public FhirContext getFhirContext() { 157 return myCtx; 158 } 159 160 @Override 161 public String getName() { 162 return getFhirContext().getVersion().getVersion() + " Snapshot Generating Validation Support"; 163 } 164}