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 org.apache.commons.compress.utils.Sets;
008import org.apache.commons.lang3.Validate;
009import org.hl7.fhir.instance.model.api.IBase;
010import org.hl7.fhir.instance.model.api.IBaseResource;
011import org.hl7.fhir.instance.model.api.IPrimitiveType;
012import org.hl7.fhir.r4.model.CodeSystem;
013import org.hl7.fhir.r4.model.StructureDefinition;
014import org.hl7.fhir.r4.model.ValueSet;
015
016import javax.annotation.Nonnull;
017import java.util.ArrayList;
018import java.util.Collection;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.Set;
025
026import static org.apache.commons.lang3.StringUtils.isNotBlank;
027
028/**
029 * This class is an implementation of {@link IValidationSupport} which may be pre-populated
030 * with a collection of validation resources to be used by the validator.
031 */
032public class PrePopulatedValidationSupport extends BaseStaticResourceValidationSupport implements IValidationSupport {
033
034        private final Map<String, IBaseResource> myCodeSystems;
035        private final Map<String, IBaseResource> myStructureDefinitions;
036        private final Map<String, IBaseResource> myValueSets;
037        private final Map<String, byte[]> myBinaries;
038
039        /**
040         * Constructor
041         */
042        public PrePopulatedValidationSupport(FhirContext theContext) {
043                this(theContext, new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
044        }
045
046        /**
047         * Constructor
048         *
049         * @param theStructureDefinitions The StructureDefinitions to be returned by this module. Keys are the logical URL for the resource, and
050         *                                values are the resource itself.
051         * @param theValueSets            The ValueSets to be returned by this module. Keys are the logical URL for the resource, and values are
052         *                                the resource itself.
053         * @param theCodeSystems          The CodeSystems to be returned by this module. Keys are the logical URL for the resource, and values are
054         *                                the resource itself.
055         **/
056        public PrePopulatedValidationSupport(
057                FhirContext theFhirContext,
058                Map<String, IBaseResource> theStructureDefinitions,
059                Map<String, IBaseResource> theValueSets,
060                Map<String, IBaseResource> theCodeSystems) {
061                this(theFhirContext, theStructureDefinitions, theValueSets, theCodeSystems, new HashMap<>());
062        }
063
064        /**
065         * Constructor
066         *
067         * @param theStructureDefinitions The StructureDefinitions to be returned by this module. Keys are the logical URL for the resource, and
068         *                                values are the resource itself.
069         * @param theValueSets            The ValueSets to be returned by this module. Keys are the logical URL for the resource, and values are
070         *                                the resource itself.
071         * @param theCodeSystems          The CodeSystems to be returned by this module. Keys are the logical URL for the resource, and values are
072         *                                the resource itself.
073         * @param theBinaries                            The binary files to be returned by this module. Keys are the unique filename for the binary, and values
074         *                                are the contents of the file as a byte array.
075         */
076        public PrePopulatedValidationSupport(
077                FhirContext theFhirContext,
078                Map<String, IBaseResource> theStructureDefinitions,
079                Map<String, IBaseResource> theValueSets,
080                Map<String, IBaseResource> theCodeSystems,
081                Map<String, byte[]> theBinaries) {
082                super(theFhirContext);
083                Validate.notNull(theFhirContext, "theFhirContext must not be null");
084                Validate.notNull(theStructureDefinitions, "theStructureDefinitions must not be null");
085                Validate.notNull(theValueSets, "theValueSets must not be null");
086                Validate.notNull(theCodeSystems, "theCodeSystems must not be null");
087                Validate.notNull(theBinaries, "theBinaries must not be null");
088                myStructureDefinitions = theStructureDefinitions;
089                myValueSets = theValueSets;
090                myCodeSystems = theCodeSystems;
091                myBinaries = theBinaries;
092        }
093
094        public void addBinary(byte[] theBinary, String theBinaryKey) {
095                Validate.notNull(theBinary, "theBinaryKey must not be null");
096                Validate.notNull(theBinary, "the" + theBinaryKey + " must not be null");
097                myBinaries.put(theBinaryKey, theBinary);
098        }
099
100        /**
101         * Add a new CodeSystem resource which will be available to the validator. Note that
102         * {@link CodeSystem#getUrl() the URL field) in this resource must contain a value as this
103         * value will be used as the logical URL.
104         * <p>
105         * Note that if the URL is a canonical FHIR URL (e.g. http://hl7.org/StructureDefinition/Extension),
106         * it will be stored in three ways:
107         * <ul>
108         * <li>Extension</li>
109         * <li>StructureDefinition/Extension</li>
110         * <li>http://hl7.org/StructureDefinition/Extension</li>
111         * </ul>
112         * </p>
113         */
114        public void addCodeSystem(IBaseResource theCodeSystem) {
115                Set<String> urls = processResourceAndReturnUrls(theCodeSystem, "CodeSystem");
116                addToMap(theCodeSystem, myCodeSystems, urls);
117        }
118
119        private Set<String> processResourceAndReturnUrls(IBaseResource theResource, String theResourceName) {
120                Validate.notNull(theResource, "the" + theResourceName + " must not be null");
121                RuntimeResourceDefinition resourceDef = getFhirContext().getResourceDefinition(theResource);
122                String actualResourceName = resourceDef.getName();
123                Validate.isTrue(actualResourceName.equals(theResourceName), "the" + theResourceName + " must be a " + theResourceName + " - Got: " + actualResourceName);
124
125                Optional<IBase> urlValue = resourceDef.getChildByName("url").getAccessor().getFirstValueOrNull(theResource);
126                String url = urlValue.map(t -> (((IPrimitiveType<?>) t).getValueAsString())).orElse(null);
127
128                Validate.notNull(url, "the" + theResourceName + ".getUrl() must not return null");
129                Validate.notBlank(url, "the" + theResourceName + ".getUrl() must return a value");
130
131                String urlWithoutVersion;
132                int pipeIdx = url.indexOf('|');
133                if (pipeIdx != -1) {
134                        urlWithoutVersion = url.substring(0, pipeIdx);
135                } else {
136                        urlWithoutVersion = url;
137                }
138
139                HashSet<String> retVal = Sets.newHashSet(url, urlWithoutVersion);
140
141                Optional<IBase> versionValue = resourceDef.getChildByName("version").getAccessor().getFirstValueOrNull(theResource);
142                String version = versionValue.map(t -> (((IPrimitiveType<?>) t).getValueAsString())).orElse(null);
143                if (isNotBlank(version)) {
144                        retVal.add(urlWithoutVersion + "|" + version);
145                }
146
147                return retVal;
148        }
149
150        /**
151         * Add a new StructureDefinition resource which will be available to the validator. Note that
152         * {@link StructureDefinition#getUrl() the URL field) in this resource must contain a value as this
153         * value will be used as the logical URL.
154         * <p>
155         * Note that if the URL is a canonical FHIR URL (e.g. http://hl7.org/StructureDefinition/Extension),
156         * it will be stored in three ways:
157         * <ul>
158         * <li>Extension</li>
159         * <li>StructureDefinition/Extension</li>
160         * <li>http://hl7.org/StructureDefinition/Extension</li>
161         * </ul>
162         * </p>
163         */
164        public void addStructureDefinition(IBaseResource theStructureDefinition) {
165                Set<String> url = processResourceAndReturnUrls(theStructureDefinition, "StructureDefinition");
166                addToMap(theStructureDefinition, myStructureDefinitions, url);
167        }
168
169        private <T extends IBaseResource> void addToMap(T theResource, Map<String, T> theMap, Collection<String> theUrls) {
170                for (String urls : theUrls) {
171                        if (isNotBlank(urls)) {
172                                theMap.put(urls, theResource);
173
174                                int lastSlashIdx = urls.lastIndexOf('/');
175                                if (lastSlashIdx != -1) {
176                                        theMap.put(urls.substring(lastSlashIdx + 1), theResource);
177                                        int previousSlashIdx = urls.lastIndexOf('/', lastSlashIdx - 1);
178                                        if (previousSlashIdx != -1) {
179                                                theMap.put(urls.substring(previousSlashIdx + 1), theResource);
180                                        }
181                                }
182                        }
183                }
184        }
185
186        /**
187         * Add a new ValueSet resource which will be available to the validator. Note that
188         * {@link ValueSet#getUrl() the URL field) in this resource must contain a value as this
189         * value will be used as the logical URL.
190         * <p>
191         * Note that if the URL is a canonical FHIR URL (e.g. http://hl7.org/StructureDefinition/Extension),
192         * it will be stored in three ways:
193         * <ul>
194         * <li>Extension</li>
195         * <li>StructureDefinition/Extension</li>
196         * <li>http://hl7.org/StructureDefinition/Extension</li>
197         * </ul>
198         * </p>
199         */
200        public void addValueSet(IBaseResource theValueSet) {
201                Set<String> urls = processResourceAndReturnUrls(theValueSet, "ValueSet");
202                addToMap(theValueSet, myValueSets, urls);
203        }
204
205
206        /**
207         * @param theResource The resource. This method delegates to the type-specific methods (e.g. {@link #addCodeSystem(IBaseResource)})
208         *                    and will do nothing if the resource type is not supported by this class.
209         * @since 5.5.0
210         */
211        public void addResource(@Nonnull IBaseResource theResource) {
212                Validate.notNull(theResource, "theResource must not be null");
213
214                switch (getFhirContext().getResourceType(theResource)) {
215                        case "StructureDefinition":
216                                addStructureDefinition(theResource);
217                                break;
218                        case "CodeSystem":
219                                addCodeSystem(theResource);
220                                break;
221                        case "ValueSet":
222                                addValueSet(theResource);
223                                break;
224                }
225        }
226
227        @Override
228        public List<IBaseResource> fetchAllConformanceResources() {
229                ArrayList<IBaseResource> retVal = new ArrayList<>();
230                retVal.addAll(myCodeSystems.values());
231                retVal.addAll(myStructureDefinitions.values());
232                retVal.addAll(myValueSets.values());
233                return retVal;
234        }
235
236        @Override
237        public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() {
238                return toList(myStructureDefinitions);
239        }
240
241        @Override
242        public IBaseResource fetchCodeSystem(String theSystem) {
243                return myCodeSystems.get(theSystem);
244        }
245
246        @Override
247        public IBaseResource fetchValueSet(String theUri) {
248                return myValueSets.get(theUri);
249        }
250
251        @Override
252        public IBaseResource fetchStructureDefinition(String theUrl) {
253                return myStructureDefinitions.get(theUrl);
254        }
255
256        @Override
257        public byte[] fetchBinary(String theBinaryKey) { return myBinaries.get(theBinaryKey); }
258
259        @Override
260        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
261                return myCodeSystems.containsKey(theSystem);
262        }
263
264        @Override
265        public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
266                return myValueSets.containsKey(theValueSetUrl);
267        }
268}