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}