001package org.hl7.fhir.common.hapi.validation.support;
002
003import ca.uhn.fhir.context.FhirContext;
004import ca.uhn.fhir.context.RuntimeResourceDefinition;
005import ca.uhn.fhir.context.support.IValidationSupport;
006import ca.uhn.fhir.context.support.ValidationSupportContext;
007import ca.uhn.fhir.util.ILockable;
008import jakarta.annotation.Nonnull;
009import jakarta.annotation.Nullable;
010import org.apache.commons.compress.utils.Sets;
011import org.apache.commons.lang3.Validate;
012import org.hl7.fhir.instance.model.api.IBase;
013import org.hl7.fhir.instance.model.api.IBaseResource;
014import org.hl7.fhir.instance.model.api.IPrimitiveType;
015import org.hl7.fhir.r4.model.CodeSystem;
016import org.hl7.fhir.r4.model.StructureDefinition;
017import org.hl7.fhir.r4.model.ValueSet;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Optional;
027import java.util.Set;
028import java.util.stream.Collectors;
029
030import static org.apache.commons.lang3.StringUtils.isNotBlank;
031
032/**
033 * This class is an implementation of {@link IValidationSupport} which may be pre-populated
034 * with a collection of validation resources to be used by the validator.
035 */
036public class PrePopulatedValidationSupport extends BaseValidationSupport implements IValidationSupport, ILockable {
037
038        private final Map<String, IBaseResource> myUrlToCodeSystems;
039        private final Map<String, IBaseResource> myUrlToStructureDefinitions;
040        private final Map<String, IBaseResource> myUrlToSearchParameters;
041        private final Map<String, IBaseResource> myUrlToValueSets;
042        private final List<IBaseResource> myCodeSystems;
043        private final List<IBaseResource> myStructureDefinitions;
044        private final List<IBaseResource> mySearchParameters;
045        private final List<IBaseResource> myValueSets;
046        private final Map<String, byte[]> myBinaries;
047        private boolean myLocked;
048
049        /**
050         * Constructor
051         */
052        public PrePopulatedValidationSupport(FhirContext theContext) {
053                this(theContext, new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
054        }
055
056        @Override
057        public String getName() {
058                return getFhirContext().getVersion().getVersion() + " Pre-populated Validation Support";
059        }
060
061        /**
062         * Constructor
063         *
064         * @param theUrlToStructureDefinitions The StructureDefinitions to be returned by this module. Keys are the logical URL for the resource, and
065         *                                values are the resource itself.
066         * @param theUrlToValueSets            The ValueSets to be returned by this module. Keys are the logical URL for the resource, and values are
067         *                                the resource itself.
068         * @param theUrlToCodeSystems          The CodeSystems to be returned by this module. Keys are the logical URL for the resource, and values are
069         *                                the resource itself.
070         **/
071        public PrePopulatedValidationSupport(
072                        FhirContext theFhirContext,
073                        Map<String, IBaseResource> theUrlToStructureDefinitions,
074                        Map<String, IBaseResource> theUrlToValueSets,
075                        Map<String, IBaseResource> theUrlToCodeSystems) {
076                this(
077                                theFhirContext,
078                                theUrlToStructureDefinitions,
079                                theUrlToValueSets,
080                                theUrlToCodeSystems,
081                                new HashMap<>(),
082                                new HashMap<>());
083        }
084
085        /**
086         * Constructor
087         *
088         * @param theUrlToStructureDefinitions The StructureDefinitions to be returned by this module. Keys are the logical URL for the resource, and
089         *                                values are the resource itself.
090         * @param theUrlToValueSets            The ValueSets to be returned by this module. Keys are the logical URL for the resource, and values are
091         *                                the resource itself.
092         * @param theUrlToCodeSystems          The CodeSystems to be returned by this module. Keys are the logical URL for the resource, and values are
093         *                                the resource itself.
094         * @param theBinaries                            The binary files to be returned by this module. Keys are the unique filename for the binary, and values
095         *                                are the contents of the file as a byte array.
096         */
097        public PrePopulatedValidationSupport(
098                        FhirContext theFhirContext,
099                        Map<String, IBaseResource> theUrlToStructureDefinitions,
100                        Map<String, IBaseResource> theUrlToValueSets,
101                        Map<String, IBaseResource> theUrlToCodeSystems,
102                        Map<String, IBaseResource> theUrlToSearchParameters,
103                        Map<String, byte[]> theBinaries) {
104                super(theFhirContext);
105                Validate.notNull(theFhirContext, "theFhirContext must not be null");
106                Validate.notNull(theUrlToStructureDefinitions, "theStructureDefinitions must not be null");
107                Validate.notNull(theUrlToValueSets, "theValueSets must not be null");
108                Validate.notNull(theUrlToCodeSystems, "theCodeSystems must not be null");
109                Validate.notNull(theUrlToSearchParameters, "theSearchParameters must not be null");
110                Validate.notNull(theBinaries, "theBinaries must not be null");
111                myUrlToStructureDefinitions = theUrlToStructureDefinitions;
112                myStructureDefinitions =
113                                theUrlToStructureDefinitions.values().stream().distinct().collect(Collectors.toList());
114
115                myUrlToValueSets = theUrlToValueSets;
116                myValueSets = theUrlToValueSets.values().stream().distinct().collect(Collectors.toList());
117
118                myUrlToCodeSystems = theUrlToCodeSystems;
119                myCodeSystems = theUrlToCodeSystems.values().stream().distinct().collect(Collectors.toList());
120
121                myUrlToSearchParameters = theUrlToSearchParameters;
122                mySearchParameters =
123                                theUrlToSearchParameters.values().stream().distinct().collect(Collectors.toList());
124
125                myBinaries = theBinaries;
126        }
127
128        public void addBinary(byte[] theBinary, String theBinaryKey) {
129                validateNotLocked();
130                Validate.notNull(theBinary, "theBinaryKey must not be null");
131                Validate.notNull(theBinary, "the" + theBinaryKey + " must not be null");
132                myBinaries.put(theBinaryKey, theBinary);
133        }
134
135        private synchronized void validateNotLocked() {
136                Validate.isTrue(myLocked == false, "Can not add to validation support, module is locked");
137        }
138
139        /**
140         * Add a new CodeSystem resource which will be available to the validator. Note that
141         * {@link CodeSystem#getUrl() the URL field) in this resource must contain a value as this
142         * value will be used as the logical URL.
143         * <p>
144         * Note that if the URL is a canonical FHIR URL (e.g. http://hl7.org/StructureDefinition/Extension),
145         * it will be stored in three ways:
146         * <ul>
147         * <li>Extension</li>
148         * <li>StructureDefinition/Extension</li>
149         * <li>http://hl7.org/StructureDefinition/Extension</li>
150         * </ul>
151         * </p>
152         */
153        public void addCodeSystem(IBaseResource theCodeSystem) {
154                validateNotLocked();
155                Set<String> urls = processResourceAndReturnUrls(theCodeSystem, "CodeSystem");
156                addToMap(theCodeSystem, myCodeSystems, myUrlToCodeSystems, urls);
157        }
158
159        private Set<String> processResourceAndReturnUrls(IBaseResource theResource, String theResourceName) {
160                Validate.notNull(theResource, "the" + theResourceName + " must not be null");
161                RuntimeResourceDefinition resourceDef = getFhirContext().getResourceDefinition(theResource);
162                String actualResourceName = resourceDef.getName();
163                Validate.isTrue(
164                                actualResourceName.equals(theResourceName),
165                                "the" + theResourceName + " must be a " + theResourceName + " - Got: " + actualResourceName);
166
167                Optional<IBase> urlValue =
168                                resourceDef.getChildByName("url").getAccessor().getFirstValueOrNull(theResource);
169                String url =
170                                urlValue.map(t -> (((IPrimitiveType<?>) t).getValueAsString())).orElse(null);
171
172                Validate.notNull(url, "the" + theResourceName + ".getUrl() must not return null");
173                Validate.notBlank(url, "the" + theResourceName + ".getUrl() must return a value");
174
175                String urlWithoutVersion;
176                int pipeIdx = url.indexOf('|');
177                if (pipeIdx != -1) {
178                        urlWithoutVersion = url.substring(0, pipeIdx);
179                } else {
180                        urlWithoutVersion = url;
181                }
182
183                HashSet<String> retVal = Sets.newHashSet(url, urlWithoutVersion);
184
185                Optional<IBase> versionValue =
186                                resourceDef.getChildByName("version").getAccessor().getFirstValueOrNull(theResource);
187                String version = versionValue
188                                .map(t -> (((IPrimitiveType<?>) t).getValueAsString()))
189                                .orElse(null);
190                if (isNotBlank(version)) {
191                        retVal.add(urlWithoutVersion + "|" + version);
192                }
193
194                return retVal;
195        }
196
197        /**
198         * Add a new StructureDefinition resource which will be available to the validator. Note that
199         * {@link StructureDefinition#getUrl() the URL field) in this resource must contain a value as this
200         * value will be used as the logical URL.
201         * <p>
202         * Note that if the URL is a canonical FHIR URL (e.g. http://hl7.org/StructureDefinition/Extension),
203         * it will be stored in three ways:
204         * <ul>
205         * <li>Extension</li>
206         * <li>StructureDefinition/Extension</li>
207         * <li>http://hl7.org/StructureDefinition/Extension</li>
208         * </ul>
209         * </p>
210         */
211        public void addStructureDefinition(IBaseResource theStructureDefinition) {
212                validateNotLocked();
213                Set<String> url = processResourceAndReturnUrls(theStructureDefinition, "StructureDefinition");
214                addToMap(theStructureDefinition, myStructureDefinitions, myUrlToStructureDefinitions, url);
215        }
216
217        public void addSearchParameter(IBaseResource theSearchParameter) {
218                validateNotLocked();
219                Set<String> url = processResourceAndReturnUrls(theSearchParameter, "SearchParameter");
220                addToMap(theSearchParameter, mySearchParameters, myUrlToSearchParameters, url);
221        }
222
223        private <T extends IBaseResource> void addToMap(
224                        T theResource, List<T> theList, Map<String, T> theMap, Collection<String> theUrls) {
225                theList.add(theResource);
226                for (String urls : theUrls) {
227                        if (isNotBlank(urls)) {
228                                theMap.put(urls, theResource);
229
230                                int lastSlashIdx = urls.lastIndexOf('/');
231                                if (lastSlashIdx != -1) {
232                                        theMap.put(urls.substring(lastSlashIdx + 1), theResource);
233                                        int previousSlashIdx = urls.lastIndexOf('/', lastSlashIdx - 1);
234                                        if (previousSlashIdx != -1) {
235                                                theMap.put(urls.substring(previousSlashIdx + 1), theResource);
236                                        }
237                                }
238                        }
239                }
240        }
241
242        /**
243         * Add a new ValueSet resource which will be available to the validator. Note that
244         * {@link ValueSet#getUrl() the URL field) in this resource must contain a value as this
245         * value will be used as the logical URL.
246         * <p>
247         * Note that if the URL is a canonical FHIR URL (e.g. http://hl7.org/StructureDefinition/Extension),
248         * it will be stored in three ways:
249         * <ul>
250         * <li>Extension</li>
251         * <li>StructureDefinition/Extension</li>
252         * <li>http://hl7.org/StructureDefinition/Extension</li>
253         * </ul>
254         * </p>
255         */
256        public void addValueSet(IBaseResource theValueSet) {
257                validateNotLocked();
258                Set<String> urls = processResourceAndReturnUrls(theValueSet, "ValueSet");
259                addToMap(theValueSet, myValueSets, myUrlToValueSets, urls);
260        }
261
262        /**
263         * @param theResource The resource. This method delegates to the type-specific methods (e.g. {@link #addCodeSystem(IBaseResource)})
264         *                    and will do nothing if the resource type is not supported by this class.
265         * @since 5.5.0
266         */
267        public void addResource(@Nonnull IBaseResource theResource) {
268                validateNotLocked();
269                Validate.notNull(theResource, "theResource must not be null");
270
271                switch (getFhirContext().getResourceType(theResource)) {
272                        case "SearchParameter":
273                                addSearchParameter(theResource);
274                                break;
275                        case "StructureDefinition":
276                                addStructureDefinition(theResource);
277                                break;
278                        case "CodeSystem":
279                                addCodeSystem(theResource);
280                                break;
281                        case "ValueSet":
282                                addValueSet(theResource);
283                                break;
284                }
285        }
286
287        @Override
288        public List<IBaseResource> fetchAllConformanceResources() {
289                ArrayList<IBaseResource> retVal = new ArrayList<>();
290                retVal.addAll(myCodeSystems);
291                retVal.addAll(myStructureDefinitions);
292                retVal.addAll(myValueSets);
293                return retVal;
294        }
295
296        @SuppressWarnings("unchecked")
297        @Nullable
298        @Override
299        public <T extends IBaseResource> List<T> fetchAllSearchParameters() {
300                return (List<T>) Collections.unmodifiableList(mySearchParameters);
301        }
302
303        @SuppressWarnings("unchecked")
304        @Override
305        public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() {
306                return (List<T>) Collections.unmodifiableList(myStructureDefinitions);
307        }
308
309        @Override
310        public IBaseResource fetchCodeSystem(String theSystem) {
311                return myUrlToCodeSystems.get(theSystem);
312        }
313
314        @Override
315        public IBaseResource fetchValueSet(String theUri) {
316                return myUrlToValueSets.get(theUri);
317        }
318
319        @Override
320        public IBaseResource fetchStructureDefinition(String theUrl) {
321                return myUrlToStructureDefinitions.get(theUrl);
322        }
323
324        @Override
325        public byte[] fetchBinary(String theBinaryKey) {
326                return myBinaries.get(theBinaryKey);
327        }
328
329        @Override
330        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
331                return myUrlToCodeSystems.containsKey(theSystem);
332        }
333
334        @Override
335        public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
336                return myUrlToValueSets.containsKey(theValueSetUrl);
337        }
338
339        /**
340         * Returns a count of all known resources
341         */
342        public int countAll() {
343                return myBinaries.size()
344                                + myCodeSystems.size()
345                                + myStructureDefinitions.size()
346                                + myValueSets.size()
347                                + myStructureDefinitions.size();
348        }
349
350        @Override
351        public synchronized void lock() {
352                myLocked = true;
353        }
354}