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