001package org.hl7.fhir.common.hapi.validation.support;
002
003import ca.uhn.fhir.context.FhirContext;
004import ca.uhn.fhir.context.FhirVersionEnum;
005import ca.uhn.fhir.context.support.ConceptValidationOptions;
006import ca.uhn.fhir.context.support.IValidationSupport;
007import ca.uhn.fhir.context.support.LookupCodeRequest;
008import ca.uhn.fhir.context.support.ValidationSupportContext;
009import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
010import ca.uhn.fhir.i18n.Msg;
011import ca.uhn.fhir.util.FhirVersionIndependentConcept;
012import ca.uhn.hapi.converters.canonical.VersionCanonicalizer;
013import com.google.common.annotations.VisibleForTesting;
014import jakarta.annotation.Nonnull;
015import jakarta.annotation.Nullable;
016import org.apache.commons.lang3.Validate;
017import org.hl7.fhir.dstu2.model.ValueSet;
018import org.hl7.fhir.instance.model.api.IBaseResource;
019import org.hl7.fhir.instance.model.api.IPrimitiveType;
020import org.hl7.fhir.r5.model.CanonicalType;
021import org.hl7.fhir.r5.model.CodeSystem;
022import org.hl7.fhir.r5.model.Enumerations;
023
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Objects;
029import java.util.Optional;
030import java.util.Set;
031import java.util.function.Consumer;
032import java.util.function.Function;
033import java.util.stream.Collectors;
034
035import static org.apache.commons.lang3.StringUtils.contains;
036import static org.apache.commons.lang3.StringUtils.defaultString;
037import static org.apache.commons.lang3.StringUtils.isBlank;
038import static org.apache.commons.lang3.StringUtils.isNotBlank;
039import static org.apache.commons.lang3.StringUtils.substringAfter;
040import static org.apache.commons.lang3.StringUtils.substringBefore;
041import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.getFhirVersionEnum;
042
043/**
044 * This class is a basic in-memory terminology service, designed to expand ValueSets and validate codes
045 * completely in-memory. It is suitable for runtime validation purposes where no dedicated terminology
046 * service exists (either an internal one such as the HAPI FHIR JPA terminology service, or an
047 * external term service API)
048 */
049@SuppressWarnings("EnhancedSwitchMigration")
050public class InMemoryTerminologyServerValidationSupport implements IValidationSupport {
051        private static final String OUR_PIPE_CHARACTER = "|";
052        private final FhirContext myCtx;
053        private VersionCanonicalizer myVersionCanonicalizer;
054        private IssueSeverity myIssueSeverityForCodeDisplayMismatch = IssueSeverity.WARNING;
055
056        /**
057         * Constructor
058         *
059         * @param theCtx A FhirContext for the FHIR version being validated
060         */
061        public InMemoryTerminologyServerValidationSupport(FhirContext theCtx) {
062                Validate.notNull(theCtx, "theCtx must not be null");
063                myCtx = theCtx;
064                myVersionCanonicalizer = new VersionCanonicalizer(theCtx);
065        }
066
067        @VisibleForTesting
068        public void setVersionCanonicalizer(VersionCanonicalizer theVersionCanonicalizer) {
069                myVersionCanonicalizer = theVersionCanonicalizer;
070        }
071
072        @Override
073        public String getName() {
074                return myCtx.getVersion().getVersion() + " In-Memory Validation Support";
075        }
076
077        /**
078         * This setting controls the validation issue severity to report when a code validation
079         * finds that the code is present in the given CodeSystem, but the display name being
080         * validated doesn't match the expected value(s). Defaults to
081         * {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#WARNING}. Set this
082         * value to {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#INFORMATION}
083         * if you don't want to see display name validation issues at all in resource validation
084         * outcomes.
085         *
086         * @since 7.0.0
087         */
088        public IssueSeverity getIssueSeverityForCodeDisplayMismatch() {
089                return myIssueSeverityForCodeDisplayMismatch;
090        }
091
092        /**
093         * This setting controls the validation issue severity to report when a code validation
094         * finds that the code is present in the given CodeSystem, but the display name being
095         * validated doesn't match the expected value(s). Defaults to
096         * {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#WARNING}. Set this
097         * value to {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#INFORMATION}
098         * if you don't want to see display name validation issues at all in resource validation
099         * outcomes.
100         *
101         * @param theIssueSeverityForCodeDisplayMismatch The severity. Must not be {@literal null}.
102         * @since 7.0.0
103         */
104        public void setIssueSeverityForCodeDisplayMismatch(@Nonnull IssueSeverity theIssueSeverityForCodeDisplayMismatch) {
105                Validate.notNull(
106                                theIssueSeverityForCodeDisplayMismatch, "theIssueSeverityForCodeDisplayMismatch must not be null");
107                myIssueSeverityForCodeDisplayMismatch = theIssueSeverityForCodeDisplayMismatch;
108        }
109
110        @Override
111        public FhirContext getFhirContext() {
112                return myCtx;
113        }
114
115        @Override
116        public ValueSetExpansionOutcome expandValueSet(
117                        ValidationSupportContext theValidationSupportContext,
118                        ValueSetExpansionOptions theExpansionOptions,
119                        @Nonnull IBaseResource theValueSetToExpand) {
120                return expandValueSet(theValidationSupportContext, theValueSetToExpand, null, null);
121        }
122
123        private ValueSetExpansionOutcome expandValueSet(
124                        ValidationSupportContext theValidationSupportContext,
125                        IBaseResource theValueSetToExpand,
126                        String theWantSystemAndVersion,
127                        String theWantCode) {
128                org.hl7.fhir.r5.model.ValueSet expansionR5;
129                try {
130                        expansionR5 = expandValueSetToCanonical(
131                                                        theValidationSupportContext, theValueSetToExpand, theWantSystemAndVersion, theWantCode)
132                                        .getValueSet();
133                } catch (ExpansionCouldNotBeCompletedInternallyException e) {
134                        return new ValueSetExpansionOutcome(e.getMessage(), false);
135                }
136                if (expansionR5 == null) {
137                        return null;
138                }
139
140                IBaseResource expansion = myVersionCanonicalizer.valueSetFromValidatorCanonical(expansionR5);
141                return new ValueSetExpansionOutcome(expansion);
142        }
143
144        private ValueSetAndMessages expandValueSetToCanonical(
145                        ValidationSupportContext theValidationSupportContext,
146                        IBaseResource theValueSetToExpand,
147                        @Nullable String theWantSystemUrlAndVersion,
148                        @Nullable String theWantCode)
149                        throws ExpansionCouldNotBeCompletedInternallyException {
150                org.hl7.fhir.r5.model.ValueSet input = myVersionCanonicalizer.valueSetToValidatorCanonical(theValueSetToExpand);
151                return expandValueSetR5(theValidationSupportContext, input, theWantSystemUrlAndVersion, theWantCode);
152        }
153
154        @Override
155        public CodeValidationResult validateCodeInValueSet(
156                        ValidationSupportContext theValidationSupportContext,
157                        ConceptValidationOptions theOptions,
158                        String theCodeSystemUrlAndVersion,
159                        String theCode,
160                        String theDisplay,
161                        @Nonnull IBaseResource theValueSet) {
162                ValueSetAndMessages expansion;
163                String vsUrl = CommonCodeSystemsTerminologyService.getValueSetUrl(getFhirContext(), theValueSet);
164                try {
165                        expansion = expandValueSetToCanonical(
166                                        theValidationSupportContext, theValueSet, theCodeSystemUrlAndVersion, theCode);
167                } catch (ExpansionCouldNotBeCompletedInternallyException e) {
168                        CodeValidationResult codeValidationResult = new CodeValidationResult();
169                        codeValidationResult.setSeverity(IssueSeverity.ERROR);
170
171                        String msg = "Failed to expand ValueSet '" + vsUrl + "' (in-memory). Could not validate code "
172                                        + theCodeSystemUrlAndVersion + "#" + theCode;
173                        if (e.getMessage() != null) {
174                                msg += ". Error was: " + e.getMessage();
175                        }
176
177                        codeValidationResult.setMessage(msg);
178                        codeValidationResult.addIssue(e.getCodeValidationIssue());
179                        return codeValidationResult;
180                }
181
182                if (expansion == null || expansion.getValueSet() == null) {
183                        return null;
184                }
185
186                if (expansion.getValueSet().getExpansion().getContains().isEmpty()) {
187                        IssueSeverity severity = IssueSeverity.ERROR;
188                        String message = "Unknown code '"
189                                        + getFormattedCodeSystemAndCodeForMessage(theCodeSystemUrlAndVersion, theCode)
190                                        + "'"
191                                        + createInMemoryExpansionMessageSuffix(vsUrl)
192                                        + (expansion.getMessages().isEmpty() ? "" : " Expansion result: " + expansion.getMessages());
193                        CodeValidationIssueCoding issueCoding = CodeValidationIssueCoding.NOT_IN_VS;
194                        CodeValidationIssueCode notFound = CodeValidationIssueCode.NOT_FOUND;
195                        CodeValidationResult codeValidationResult = new CodeValidationResult()
196                                        .setSeverity(severity)
197                                        .setMessage(message)
198                                        .setSourceDetails(null)
199                                        .addIssue(new CodeValidationIssue(message, severity, notFound, issueCoding));
200                        return codeValidationResult;
201                }
202
203                return validateCodeInExpandedValueSet(
204                                theValidationSupportContext,
205                                theOptions,
206                                theCodeSystemUrlAndVersion,
207                                theCode,
208                                theDisplay,
209                                expansion.getValueSet(),
210                                vsUrl);
211        }
212
213        @Override
214        @Nullable
215        public CodeValidationResult validateCode(
216                        @Nonnull ValidationSupportContext theValidationSupportContext,
217                        @Nonnull ConceptValidationOptions theOptions,
218                        String theCodeSystem,
219                        String theCode,
220                        String theDisplay,
221                        String theValueSetUrl) {
222                IBaseResource vs;
223                if (isNotBlank(theValueSetUrl)) {
224                        vs = theValidationSupportContext.getRootValidationSupport().fetchValueSet(theValueSetUrl);
225                        if (vs == null) {
226                                return null;
227                        }
228                } else {
229                        String codeSystemUrl;
230                        String codeSystemVersion = null;
231                        int codeSystemVersionIndex = theCodeSystem.indexOf("|");
232                        if (codeSystemVersionIndex > -1) {
233                                codeSystemUrl = theCodeSystem.substring(0, codeSystemVersionIndex);
234                                codeSystemVersion = theCodeSystem.substring(codeSystemVersionIndex + 1);
235                        } else {
236                                codeSystemUrl = theCodeSystem;
237                        }
238                        switch (myCtx.getVersion().getVersion()) {
239                                case DSTU2:
240                                case DSTU2_HL7ORG:
241                                        vs = new org.hl7.fhir.dstu2.model.ValueSet()
242                                                        .setCompose(new org.hl7.fhir.dstu2.model.ValueSet.ValueSetComposeComponent()
243                                                                        .addInclude(new org.hl7.fhir.dstu2.model.ValueSet.ConceptSetComponent()
244                                                                                        .setSystem(theCodeSystem)));
245                                        break;
246                                case DSTU3:
247                                        if (codeSystemVersion != null) {
248                                                vs = new org.hl7.fhir.dstu3.model.ValueSet()
249                                                                .setCompose(new org.hl7.fhir.dstu3.model.ValueSet.ValueSetComposeComponent()
250                                                                                .addInclude(new org.hl7.fhir.dstu3.model.ValueSet.ConceptSetComponent()
251                                                                                                .setSystem(codeSystemUrl)
252                                                                                                .setVersion(codeSystemVersion)));
253                                        } else {
254                                                vs = new org.hl7.fhir.dstu3.model.ValueSet()
255                                                                .setCompose(new org.hl7.fhir.dstu3.model.ValueSet.ValueSetComposeComponent()
256                                                                                .addInclude(new org.hl7.fhir.dstu3.model.ValueSet.ConceptSetComponent()
257                                                                                                .setSystem(theCodeSystem)));
258                                        }
259                                        break;
260                                case R4:
261                                        if (codeSystemVersion != null) {
262                                                vs = new org.hl7.fhir.r4.model.ValueSet()
263                                                                .setCompose(new org.hl7.fhir.r4.model.ValueSet.ValueSetComposeComponent()
264                                                                                .addInclude(new org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent()
265                                                                                                .setSystem(codeSystemUrl)
266                                                                                                .setVersion(codeSystemVersion)));
267                                        } else {
268                                                vs = new org.hl7.fhir.r4.model.ValueSet()
269                                                                .setCompose(new org.hl7.fhir.r4.model.ValueSet.ValueSetComposeComponent()
270                                                                                .addInclude(new org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent()
271                                                                                                .setSystem(theCodeSystem)));
272                                        }
273                                        break;
274                                case R4B:
275                                        if (codeSystemVersion != null) {
276                                                vs = new org.hl7.fhir.r4b.model.ValueSet()
277                                                                .setCompose(new org.hl7.fhir.r4b.model.ValueSet.ValueSetComposeComponent()
278                                                                                .addInclude(new org.hl7.fhir.r4b.model.ValueSet.ConceptSetComponent()
279                                                                                                .setSystem(codeSystemUrl)
280                                                                                                .setVersion(codeSystemVersion)));
281                                        } else {
282                                                vs = new org.hl7.fhir.r4b.model.ValueSet()
283                                                                .setCompose(new org.hl7.fhir.r4b.model.ValueSet.ValueSetComposeComponent()
284                                                                                .addInclude(new org.hl7.fhir.r4b.model.ValueSet.ConceptSetComponent()
285                                                                                                .setSystem(theCodeSystem)));
286                                        }
287                                        break;
288                                case R5:
289                                        if (codeSystemVersion != null) {
290                                                vs = new org.hl7.fhir.r5.model.ValueSet()
291                                                                .setCompose(new org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent()
292                                                                                .addInclude(new org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent()
293                                                                                                .setSystem(codeSystemUrl)
294                                                                                                .setVersion(codeSystemVersion)));
295                                        } else {
296                                                vs = new org.hl7.fhir.r5.model.ValueSet()
297                                                                .setCompose(new org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent()
298                                                                                .addInclude(new org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent()
299                                                                                                .setSystem(theCodeSystem)));
300                                        }
301                                        break;
302                                case DSTU2_1:
303                                default:
304                                        throw new IllegalArgumentException(Msg.code(699) + "Can not handle version: "
305                                                        + myCtx.getVersion().getVersion());
306                        }
307                }
308
309                ValueSetExpansionOutcome valueSetExpansionOutcome =
310                                expandValueSet(theValidationSupportContext, vs, theCodeSystem, theCode);
311                if (valueSetExpansionOutcome == null) {
312                        return null;
313                }
314
315                if (valueSetExpansionOutcome.getError() != null) {
316                        return new CodeValidationResult()
317                                        .setSeverity(IssueSeverity.ERROR)
318                                        .setMessage(valueSetExpansionOutcome.getError());
319                }
320
321                IBaseResource expansion = valueSetExpansionOutcome.getValueSet();
322                return validateCodeInExpandedValueSet(
323                                theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, expansion, theValueSetUrl);
324        }
325
326        private CodeValidationResult validateCodeInExpandedValueSet(
327                        ValidationSupportContext theValidationSupportContext,
328                        ConceptValidationOptions theOptions,
329                        String theCodeSystemUrlAndVersionToValidate,
330                        String theCodeToValidate,
331                        String theDisplayToValidate,
332                        IBaseResource theExpansion,
333                        String theValueSetUrl) {
334                assert theExpansion != null;
335
336                final CodeValidationResult codeValidationResult;
337
338                boolean caseSensitive = true;
339                IBaseResource codeSystemToValidateResource = null;
340                if (!theOptions.isInferSystem() && isNotBlank(theCodeSystemUrlAndVersionToValidate)) {
341                        codeSystemToValidateResource = theValidationSupportContext
342                                        .getRootValidationSupport()
343                                        .fetchCodeSystem(theCodeSystemUrlAndVersionToValidate);
344                }
345
346                List<FhirVersionIndependentConcept> codes = new ArrayList<>();
347                switch (getFhirVersionEnum(
348                                theValidationSupportContext.getRootValidationSupport().getFhirContext(), theExpansion)) {
349                        case DSTU2: {
350                                ca.uhn.fhir.model.dstu2.resource.ValueSet expansionVs =
351                                                (ca.uhn.fhir.model.dstu2.resource.ValueSet) theExpansion;
352                                List<ca.uhn.fhir.model.dstu2.resource.ValueSet.ExpansionContains> contains =
353                                                expansionVs.getExpansion().getContains();
354                                flattenAndConvertCodesDstu2(contains, codes);
355                                break;
356                        }
357                        case DSTU2_HL7ORG: {
358                                ValueSet expansionVs = (ValueSet) theExpansion;
359                                List<ValueSet.ValueSetExpansionContainsComponent> contains =
360                                                expansionVs.getExpansion().getContains();
361                                flattenAndConvertCodesDstu2Hl7Org(contains, codes);
362                                break;
363                        }
364                        case DSTU3: {
365                                org.hl7.fhir.dstu3.model.ValueSet expansionVs = (org.hl7.fhir.dstu3.model.ValueSet) theExpansion;
366                                List<org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent> contains =
367                                                expansionVs.getExpansion().getContains();
368                                flattenAndConvertCodesDstu3(contains, codes);
369                                break;
370                        }
371                        case R4: {
372                                org.hl7.fhir.r4.model.ValueSet expansionVs = (org.hl7.fhir.r4.model.ValueSet) theExpansion;
373                                List<org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent> contains =
374                                                expansionVs.getExpansion().getContains();
375                                flattenAndConvertCodesR4(contains, codes);
376                                break;
377                        }
378                        case R4B: {
379                                org.hl7.fhir.r4b.model.ValueSet expansionVs = (org.hl7.fhir.r4b.model.ValueSet) theExpansion;
380                                List<org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionContainsComponent> contains =
381                                                expansionVs.getExpansion().getContains();
382                                flattenAndConvertCodesR4B(contains, codes);
383                                break;
384                        }
385                        case R5: {
386                                org.hl7.fhir.r5.model.ValueSet expansionVs = (org.hl7.fhir.r5.model.ValueSet) theExpansion;
387                                List<org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent> contains =
388                                                expansionVs.getExpansion().getContains();
389                                flattenAndConvertCodesR5(contains, codes);
390                                break;
391                        }
392                        case DSTU2_1:
393                        default:
394                                throw new IllegalArgumentException(Msg.code(700) + "Can not handle version: "
395                                                + myCtx.getVersion().getVersion());
396                }
397
398                String codeSystemResourceName = null;
399                String codeSystemResourceVersion = null;
400                String codeSystemResourceContentMode = null;
401                if (codeSystemToValidateResource != null) {
402                        switch (getFhirVersionEnum(
403                                        theValidationSupportContext.getRootValidationSupport().getFhirContext(),
404                                        codeSystemToValidateResource)) {
405                                case DSTU2:
406                                case DSTU2_HL7ORG: {
407                                        caseSensitive = true;
408                                        break;
409                                }
410                                case DSTU3: {
411                                        org.hl7.fhir.dstu3.model.CodeSystem systemDstu3 =
412                                                        (org.hl7.fhir.dstu3.model.CodeSystem) codeSystemToValidateResource;
413                                        caseSensitive = systemDstu3.getCaseSensitive();
414                                        codeSystemResourceName = systemDstu3.getName();
415                                        codeSystemResourceVersion = systemDstu3.getVersion();
416                                        codeSystemResourceContentMode =
417                                                        systemDstu3.getContentElement().getValueAsString();
418                                        break;
419                                }
420                                case R4: {
421                                        org.hl7.fhir.r4.model.CodeSystem systemR4 =
422                                                        (org.hl7.fhir.r4.model.CodeSystem) codeSystemToValidateResource;
423                                        caseSensitive = systemR4.getCaseSensitive();
424                                        codeSystemResourceName = systemR4.getName();
425                                        codeSystemResourceVersion = systemR4.getVersion();
426                                        codeSystemResourceContentMode = systemR4.getContentElement().getValueAsString();
427                                        break;
428                                }
429                                case R4B: {
430                                        org.hl7.fhir.r4b.model.CodeSystem systemR4B =
431                                                        (org.hl7.fhir.r4b.model.CodeSystem) codeSystemToValidateResource;
432                                        caseSensitive = systemR4B.getCaseSensitive();
433                                        codeSystemResourceName = systemR4B.getName();
434                                        codeSystemResourceVersion = systemR4B.getVersion();
435                                        codeSystemResourceContentMode =
436                                                        systemR4B.getContentElement().getValueAsString();
437                                        break;
438                                }
439                                case R5: {
440                                        CodeSystem systemR5 = (CodeSystem) codeSystemToValidateResource;
441                                        caseSensitive = systemR5.getCaseSensitive();
442                                        codeSystemResourceName = systemR5.getName();
443                                        codeSystemResourceVersion = systemR5.getVersion();
444                                        codeSystemResourceContentMode = systemR5.getContentElement().getValueAsString();
445                                        break;
446                                }
447                                case DSTU2_1:
448                                default:
449                                        throw new IllegalArgumentException(Msg.code(701) + "Can not handle version: "
450                                                        + myCtx.getVersion().getVersion());
451                        }
452                }
453
454                String codeSystemUrlToValidate = null;
455                String codeSystemVersionToValidate = null;
456                if (theCodeSystemUrlAndVersionToValidate != null) {
457                        int versionIndex = theCodeSystemUrlAndVersionToValidate.indexOf("|");
458                        if (versionIndex > -1) {
459                                codeSystemUrlToValidate = theCodeSystemUrlAndVersionToValidate.substring(0, versionIndex);
460                                codeSystemVersionToValidate = theCodeSystemUrlAndVersionToValidate.substring(versionIndex + 1);
461                        } else {
462                                codeSystemUrlToValidate = theCodeSystemUrlAndVersionToValidate;
463                        }
464                }
465                CodeValidationResult valueSetResult = findCodeInExpansion(
466                                theCodeToValidate,
467                                theDisplayToValidate,
468                                theValueSetUrl,
469                                codeSystemUrlToValidate,
470                                codeSystemVersionToValidate,
471                                codeSystemResourceName,
472                                codeSystemResourceVersion,
473                                codes,
474                                theOptions,
475                                caseSensitive);
476                if (valueSetResult != null) {
477                        codeValidationResult = valueSetResult;
478                } else {
479                        IValidationSupport.IssueSeverity severity;
480                        String message;
481                        CodeValidationIssueCode issueCode = CodeValidationIssueCode.CODE_INVALID;
482                        CodeValidationIssueCoding issueCoding = CodeValidationIssueCoding.INVALID_CODE;
483                        if ("fragment".equals(codeSystemResourceContentMode)) {
484                                severity = IValidationSupport.IssueSeverity.WARNING;
485                                message = "Unknown code in fragment CodeSystem '"
486                                                + getFormattedCodeSystemAndCodeForMessage(
487                                                                theCodeSystemUrlAndVersionToValidate, theCodeToValidate)
488                                                + "'";
489                        } else {
490                                severity = IValidationSupport.IssueSeverity.ERROR;
491                                message = "Unknown code '"
492                                                + getFormattedCodeSystemAndCodeForMessage(
493                                                                theCodeSystemUrlAndVersionToValidate, theCodeToValidate)
494                                                + "'";
495                        }
496                        if (isNotBlank(theValueSetUrl)) {
497                                message += createInMemoryExpansionMessageSuffix(theValueSetUrl);
498                                issueCoding = CodeValidationIssueCoding.NOT_IN_VS;
499                        }
500
501                        String sourceDetails = "In-memory expansion containing " + codes.size() + " codes";
502                        if (!codes.isEmpty() && codes.size() < 10) {
503                                sourceDetails += ": "
504                                                + codes.stream()
505                                                                .map(t -> t.getSystem() + "#" + t.getCode())
506                                                                .collect(Collectors.joining(", "));
507                        }
508
509                        codeValidationResult = new CodeValidationResult()
510                                        .setSeverity(severity)
511                                        .setMessage(message)
512                                        .setSourceDetails(sourceDetails)
513                                        .addIssue(new CodeValidationIssue(message, severity, issueCode, issueCoding));
514                }
515
516                return codeValidationResult;
517        }
518
519        @Nonnull
520        private static String createInMemoryExpansionMessageSuffix(String theValueSetUrl) {
521                return " for in-memory expansion of ValueSet '" + theValueSetUrl + "'";
522        }
523
524        private static String getFormattedCodeSystemAndCodeForMessage(
525                        String theCodeSystemUrlAndVersionToValidate, String theCodeToValidate) {
526                return (isNotBlank(theCodeSystemUrlAndVersionToValidate) ? theCodeSystemUrlAndVersionToValidate + "#" : "")
527                                + theCodeToValidate;
528        }
529
530        private CodeValidationResult findCodeInExpansion(
531                        String theCodeToValidate,
532                        String theDisplayToValidate,
533                        String theValueSetUrl,
534                        String codeSystemUrlToValidate,
535                        String codeSystemVersionToValidate,
536                        String codeSystemResourceName,
537                        String codeSystemResourceVersion,
538                        List<FhirVersionIndependentConcept> expansionCodes,
539                        ConceptValidationOptions theOptions,
540                        boolean caseSensitive) {
541                for (FhirVersionIndependentConcept nextExpansionCode : expansionCodes) {
542
543                        boolean codeMatches;
544                        if (caseSensitive) {
545                                codeMatches = defaultString(theCodeToValidate).equals(nextExpansionCode.getCode());
546                        } else {
547                                codeMatches = defaultString(theCodeToValidate).equalsIgnoreCase(nextExpansionCode.getCode());
548                        }
549                        if (codeMatches) {
550                                if (theOptions.isInferSystem()
551                                                || (nextExpansionCode.getSystem().equals(codeSystemUrlToValidate)
552                                                                && (codeSystemVersionToValidate == null
553                                                                                || codeSystemVersionToValidate.equals(nextExpansionCode.getSystemVersion())))) {
554                                        String csVersion = codeSystemResourceVersion;
555                                        if (isNotBlank(nextExpansionCode.getSystemVersion())) {
556                                                csVersion = nextExpansionCode.getSystemVersion();
557                                        }
558                                        if (!theOptions.isValidateDisplay()
559                                                        || (isBlank(nextExpansionCode.getDisplay())
560                                                                        || isBlank(theDisplayToValidate)
561                                                                        || nextExpansionCode.getDisplay().equals(theDisplayToValidate))) {
562                                                CodeValidationResult codeValidationResult = new CodeValidationResult()
563                                                                .setCode(theCodeToValidate)
564                                                                .setDisplay(nextExpansionCode.getDisplay())
565                                                                .setCodeSystemName(codeSystemResourceName)
566                                                                .setCodeSystemVersion(csVersion);
567                                                if (isNotBlank(theValueSetUrl)) {
568                                                        populateSourceDetailsForInMemoryExpansion(theValueSetUrl, codeValidationResult);
569                                                }
570                                                return codeValidationResult;
571                                        } else {
572                                                String messageAppend = "";
573                                                if (isNotBlank(theValueSetUrl)) {
574                                                        messageAppend = createInMemoryExpansionMessageSuffix(theValueSetUrl);
575                                                }
576                                                CodeValidationResult codeValidationResult = createResultForDisplayMismatch(
577                                                                myCtx,
578                                                                theCodeToValidate,
579                                                                theDisplayToValidate,
580                                                                nextExpansionCode.getDisplay(),
581                                                                codeSystemUrlToValidate,
582                                                                csVersion,
583                                                                messageAppend,
584                                                                getIssueSeverityForCodeDisplayMismatch());
585                                                if (isNotBlank(theValueSetUrl)) {
586                                                        populateSourceDetailsForInMemoryExpansion(theValueSetUrl, codeValidationResult);
587                                                }
588                                                return codeValidationResult;
589                                        }
590                                }
591                        }
592                }
593                return null;
594        }
595
596        @Override
597        public LookupCodeResult lookupCode(
598                        ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) {
599                final String code = theLookupCodeRequest.getCode();
600                final String system = theLookupCodeRequest.getSystem();
601                CodeValidationResult codeValidationResult = validateCode(
602                                theValidationSupportContext,
603                                new ConceptValidationOptions(),
604                                system,
605                                code,
606                                theLookupCodeRequest.getDisplayLanguage(),
607                                null);
608                if (codeValidationResult == null) {
609                        return null;
610                }
611                return codeValidationResult.asLookupCodeResult(system, code);
612        }
613
614        @Override
615        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
616                if (isBlank(theSystem)) {
617                        return false;
618                }
619
620                IBaseResource cs =
621                                theValidationSupportContext.getRootValidationSupport().fetchCodeSystem(theSystem);
622
623                if (!myCtx.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU2_1)) {
624                        return cs != null;
625                }
626
627                if (cs != null) {
628                        IPrimitiveType<?> content =
629                                        getFhirContext().newTerser().getSingleValueOrNull(cs, "content", IPrimitiveType.class);
630                        return !"not-present".equals(content.getValueAsString());
631                }
632
633                return false;
634        }
635
636        @Override
637        public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
638                return isNotBlank(theValueSetUrl)
639                                && theValidationSupportContext.getRootValidationSupport().fetchValueSet(theValueSetUrl) != null;
640        }
641
642        private void addCodesDstu2Hl7Org(
643                        List<ValueSet.ConceptDefinitionComponent> theSourceList,
644                        List<CodeSystem.ConceptDefinitionComponent> theTargetList) {
645                for (ValueSet.ConceptDefinitionComponent nextSource : theSourceList) {
646                        CodeSystem.ConceptDefinitionComponent targetConcept = new CodeSystem.ConceptDefinitionComponent()
647                                        .setCode(nextSource.getCode())
648                                        .setDisplay(nextSource.getDisplay());
649                        theTargetList.add(targetConcept);
650                        addCodesDstu2Hl7Org(nextSource.getConcept(), targetConcept.getConcept());
651                }
652        }
653
654        private void addCodesDstu2(
655                        List<ca.uhn.fhir.model.dstu2.resource.ValueSet.CodeSystemConcept> theSourceList,
656                        List<CodeSystem.ConceptDefinitionComponent> theTargetList) {
657                for (ca.uhn.fhir.model.dstu2.resource.ValueSet.CodeSystemConcept nextSource : theSourceList) {
658                        CodeSystem.ConceptDefinitionComponent targetConcept = new CodeSystem.ConceptDefinitionComponent()
659                                        .setCode(nextSource.getCode())
660                                        .setDisplay(nextSource.getDisplay());
661                        theTargetList.add(targetConcept);
662                        addCodesDstu2(nextSource.getConcept(), targetConcept.getConcept());
663                }
664        }
665
666        @Nullable
667        private ValueSetAndMessages expandValueSetR5(
668                        ValidationSupportContext theValidationSupportContext,
669                        org.hl7.fhir.r5.model.ValueSet theInput,
670                        @Nullable String theWantSystemUrlAndVersion,
671                        @Nullable String theWantCode)
672                        throws ExpansionCouldNotBeCompletedInternallyException {
673
674                ValueSetAndMessages retVal = new ValueSetAndMessages();
675                Set<FhirVersionIndependentConcept> concepts = new HashSet<>();
676
677                expandValueSetR5IncludeOrExcludes(
678                                theValidationSupportContext,
679                                concepts,
680                                theInput.getCompose().getInclude(),
681                                true,
682                                theWantSystemUrlAndVersion,
683                                theWantCode,
684                                retVal);
685                expandValueSetR5IncludeOrExcludes(
686                                theValidationSupportContext,
687                                concepts,
688                                theInput.getCompose().getExclude(),
689                                false,
690                                theWantSystemUrlAndVersion,
691                                theWantCode,
692                                retVal);
693
694                org.hl7.fhir.r5.model.ValueSet vs = new org.hl7.fhir.r5.model.ValueSet();
695                retVal.setValueSet(vs);
696                for (FhirVersionIndependentConcept next : concepts) {
697                        org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent contains =
698                                        vs.getExpansion().addContains();
699                        contains.setSystem(next.getSystem());
700                        contains.setCode(next.getCode());
701                        contains.setDisplay(next.getDisplay());
702                        contains.setVersion(next.getSystemVersion());
703                }
704
705                return retVal;
706        }
707
708        /**
709         * Use with caution - this is not a stable API
710         *
711         * @since 5.6.0
712         */
713        public void expandValueSetIncludeOrExclude(
714                        ValidationSupportContext theValidationSupportContext,
715                        Consumer<FhirVersionIndependentConcept> theConsumer,
716                        org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent theIncludeOrExclude)
717                        throws ExpansionCouldNotBeCompletedInternallyException {
718                expandValueSetR5IncludeOrExclude(
719                                theValidationSupportContext, theConsumer, null, null, theIncludeOrExclude, new ValueSetAndMessages());
720        }
721
722        private void expandValueSetR5IncludeOrExcludes(
723                        ValidationSupportContext theValidationSupportContext,
724                        Set<FhirVersionIndependentConcept> theConcepts,
725                        List<org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent> theComposeList,
726                        boolean theComposeListIsInclude,
727                        @Nullable String theWantSystemUrlAndVersion,
728                        @Nullable String theWantCode,
729                        ValueSetAndMessages theResponseBuilder)
730                        throws ExpansionCouldNotBeCompletedInternallyException {
731                Consumer<FhirVersionIndependentConcept> consumer = c -> {
732                        if (theComposeListIsInclude) {
733                                theConcepts.add(c);
734                        } else {
735                                theConcepts.remove(c);
736                        }
737                };
738                expandValueSetR5IncludeOrExcludes(
739                                theComposeListIsInclude,
740                                theValidationSupportContext,
741                                consumer,
742                                theComposeList,
743                                theWantSystemUrlAndVersion,
744                                theWantCode,
745                                theResponseBuilder);
746        }
747
748        private void expandValueSetR5IncludeOrExcludes(
749                        boolean theComposeListIsInclude,
750                        ValidationSupportContext theValidationSupportContext,
751                        Consumer<FhirVersionIndependentConcept> theConsumer,
752                        List<org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent> theComposeList,
753                        @Nullable String theWantSystemUrlAndVersion,
754                        @Nullable String theWantCode,
755                        ValueSetAndMessages theResponseBuilder)
756                        throws ExpansionCouldNotBeCompletedInternallyException {
757                ExpansionCouldNotBeCompletedInternallyException caughtException = null;
758                if (theComposeList.isEmpty()) {
759                        if (theComposeListIsInclude) {
760                                theResponseBuilder.addMessage("Empty compose list for includes");
761                        }
762                        return;
763                }
764                for (org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent nextInclude : theComposeList) {
765                        try {
766                                boolean outcome = expandValueSetR5IncludeOrExclude(
767                                                theValidationSupportContext,
768                                                theConsumer,
769                                                theWantSystemUrlAndVersion,
770                                                theWantCode,
771                                                nextInclude,
772                                                theResponseBuilder);
773                                if (isNotBlank(theWantCode)) {
774                                        if (outcome) {
775                                                return;
776                                        }
777                                }
778                        } catch (ExpansionCouldNotBeCompletedInternallyException e) {
779                                if (isBlank(theWantCode)) {
780                                        throw e;
781                                } else {
782                                        caughtException = e;
783                                }
784                        }
785                }
786                if (caughtException != null) {
787                        throw caughtException;
788                }
789        }
790
791        /**
792         * Returns <code>true</code> if at least one code was added
793         */
794        private boolean expandValueSetR5IncludeOrExclude(
795                        ValidationSupportContext theValidationSupportContext,
796                        Consumer<FhirVersionIndependentConcept> theConsumer,
797                        @Nullable String theWantSystemUrlAndVersion,
798                        @Nullable String theWantCode,
799                        org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent theInclude,
800                        ValueSetAndMessages theResponseBuilder)
801                        throws ExpansionCouldNotBeCompletedInternallyException {
802
803                String wantSystemUrl = null;
804                String wantSystemVersion = null;
805
806                if (theWantSystemUrlAndVersion != null) {
807                        int versionIndex = theWantSystemUrlAndVersion.indexOf(OUR_PIPE_CHARACTER);
808                        if (versionIndex > -1) {
809                                wantSystemUrl = theWantSystemUrlAndVersion.substring(0, versionIndex);
810                                wantSystemVersion = theWantSystemUrlAndVersion.substring(versionIndex + 1);
811                        } else {
812                                wantSystemUrl = theWantSystemUrlAndVersion;
813                        }
814                }
815
816                String includeOrExcludeConceptSystemUrl = theInclude.getSystem();
817                String includeOrExcludeConceptSystemVersion = theInclude.getVersion();
818
819                Function<String, CodeSystem> codeSystemLoader = newCodeSystemLoader(theValidationSupportContext);
820                Function<String, org.hl7.fhir.r5.model.ValueSet> valueSetLoader =
821                                newValueSetLoader(theValidationSupportContext);
822
823                List<FhirVersionIndependentConcept> nextCodeList = new ArrayList<>();
824                CodeSystem includeOrExcludeSystemResource = null;
825
826                if (isNotBlank(includeOrExcludeConceptSystemUrl)) {
827
828                        includeOrExcludeConceptSystemVersion = optionallyPopulateVersionFromUrl(
829                                        includeOrExcludeConceptSystemUrl, includeOrExcludeConceptSystemVersion);
830                        includeOrExcludeConceptSystemUrl = substringBefore(includeOrExcludeConceptSystemUrl, OUR_PIPE_CHARACTER);
831
832                        if (wantSystemUrl != null && !wantSystemUrl.equals(includeOrExcludeConceptSystemUrl)) {
833                                return false;
834                        }
835
836                        if (wantSystemVersion != null && !wantSystemVersion.equals(includeOrExcludeConceptSystemVersion)) {
837                                return false;
838                        }
839
840                        String loadedCodeSystemUrl;
841                        if (includeOrExcludeConceptSystemVersion != null) {
842                                loadedCodeSystemUrl =
843                                                includeOrExcludeConceptSystemUrl + OUR_PIPE_CHARACTER + includeOrExcludeConceptSystemVersion;
844                        } else {
845                                loadedCodeSystemUrl = includeOrExcludeConceptSystemUrl;
846                        }
847
848                        includeOrExcludeSystemResource = codeSystemLoader.apply(loadedCodeSystemUrl);
849
850                        boolean isIncludeWithDeclaredConcepts = !theInclude.getConcept().isEmpty();
851
852                        final Set<String> wantCodes;
853                        if (isIncludeWithDeclaredConcepts) {
854                                wantCodes = theInclude.getConcept().stream()
855                                                .map(org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent::getCode)
856                                                .collect(Collectors.toSet());
857                        } else {
858                                wantCodes = null;
859                        }
860
861                        boolean ableToHandleCode = false;
862                        String failureMessage = null;
863
864                        boolean isIncludeCodeSystemIgnored = includeOrExcludeSystemResource != null
865                                        && includeOrExcludeSystemResource.getContent() == Enumerations.CodeSystemContentMode.NOTPRESENT;
866
867                        boolean isIncludeFromSystem = isNotBlank(theInclude.getSystem())
868                                        && theInclude.getValueSet().isEmpty();
869                        boolean isIncludeWithFilter = !theInclude.getFilter().isEmpty();
870
871                        // if we can?t load the CS and we?re configured to ignore it...
872                        if (isIncludeCodeSystemIgnored && !theInclude.getFilter().isEmpty()) {
873                                // We can?t apply any structural (ISA/DESCENDENT-OF/etc..) filters if the CS is absent ? fail
874                                String msg = "Unable to expand ValueSet: cannot apply filters '" + theInclude.getFilter()
875                                                + "' because CodeSystem '" + theInclude.getSystem()
876                                                + "' is ignored/not-present";
877
878                                throw new ExpansionCouldNotBeCompletedInternallyException(
879                                                Msg.code(2646) + msg,
880                                                new CodeValidationIssue(
881                                                                msg,
882                                                                IssueSeverity.ERROR,
883                                                                CodeValidationIssueCode.NOT_FOUND,
884                                                                CodeValidationIssueCoding.NOT_FOUND));
885                        }
886
887                        if (includeOrExcludeSystemResource == null || isIncludeCodeSystemIgnored) {
888
889                                if (theWantCode != null) {
890                                        if (theValidationSupportContext
891                                                        .getRootValidationSupport()
892                                                        .isCodeSystemSupported(theValidationSupportContext, includeOrExcludeConceptSystemUrl)) {
893                                                LookupCodeResult lookup = theValidationSupportContext
894                                                                .getRootValidationSupport()
895                                                                .lookupCode(
896                                                                                theValidationSupportContext,
897                                                                                new LookupCodeRequest(includeOrExcludeConceptSystemUrl, theWantCode));
898                                                if (lookup != null) {
899                                                        ableToHandleCode = true;
900                                                        if (lookup.isFound()) {
901                                                                CodeSystem.ConceptDefinitionComponent conceptDefinition =
902                                                                                new CodeSystem.ConceptDefinitionComponent()
903                                                                                                .addConcept()
904                                                                                                .setCode(theWantCode)
905                                                                                                .setDisplay(lookup.getCodeDisplay());
906                                                                List<CodeSystem.ConceptDefinitionComponent> codesList =
907                                                                                Collections.singletonList(conceptDefinition);
908                                                                addCodes(
909                                                                                includeOrExcludeConceptSystemUrl,
910                                                                                includeOrExcludeConceptSystemVersion,
911                                                                                codesList,
912                                                                                nextCodeList,
913                                                                                wantCodes);
914                                                        }
915                                                }
916                                        } else {
917
918                                                /*
919                                                 * If we're doing an expansion specifically looking for a single code, that means we're validating that code.
920                                                 * In the case where we have a ValueSet that explicitly enumerates a collection of codes
921                                                 * (via ValueSet.compose.include.code) in a code system that is unknown we'll assume the code is valid
922                                                 * even if we can't find the CodeSystem. This is a compromise obviously, since it would be ideal for
923                                                 * CodeSystems to always be known, but realistically there are always going to be CodeSystems that
924                                                 * can't be supplied because of copyright issues, or because they are grammar based. Allowing a VS to
925                                                 * enumerate a set of good codes for them is a nice compromise there.
926                                                 */
927                                                if (Objects.equals(theInclude.getSystem(), theWantSystemUrlAndVersion)) {
928                                                        Optional<org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent>
929                                                                        matchingEnumeratedConcept = theInclude.getConcept().stream()
930                                                                                        .filter(t -> Objects.equals(t.getCode(), theWantCode))
931                                                                                        .findFirst();
932
933                                                        // If the ValueSet.compose.include has no individual concepts in it, and
934                                                        // we can't find the actual referenced CodeSystem, we have no choice
935                                                        // but to fail
936                                                        if (isIncludeWithDeclaredConcepts) {
937                                                                ableToHandleCode = true;
938                                                        } else {
939                                                                failureMessage = getFailureMessageForMissingOrUnusableCodeSystem(
940                                                                                includeOrExcludeSystemResource, loadedCodeSystemUrl);
941                                                        }
942
943                                                        if (matchingEnumeratedConcept.isPresent()) {
944                                                                CodeSystem.ConceptDefinitionComponent conceptDefinition =
945                                                                                new CodeSystem.ConceptDefinitionComponent()
946                                                                                                .addConcept()
947                                                                                                .setCode(theWantCode)
948                                                                                                .setDisplay(matchingEnumeratedConcept
949                                                                                                                .get()
950                                                                                                                .getDisplay());
951                                                                List<CodeSystem.ConceptDefinitionComponent> codesList =
952                                                                                Collections.singletonList(conceptDefinition);
953                                                                addCodes(
954                                                                                includeOrExcludeConceptSystemUrl,
955                                                                                includeOrExcludeConceptSystemVersion,
956                                                                                codesList,
957                                                                                nextCodeList,
958                                                                                wantCodes);
959                                                        }
960                                                }
961                                        }
962                                } else {
963                                        if (isIncludeFromSystem && !isIncludeWithFilter) {
964                                                if (isIncludeWithDeclaredConcepts) {
965                                                        theInclude.getConcept().stream()
966                                                                        .map(t -> new FhirVersionIndependentConcept(
967                                                                                        theInclude.getSystem(),
968                                                                                        t.getCode(),
969                                                                                        t.getDisplay(),
970                                                                                        theInclude.getVersion()))
971                                                                        .forEach(nextCodeList::add);
972                                                        ableToHandleCode = true;
973                                                } else if (isIncludeCodeSystemIgnored) {
974                                                        ableToHandleCode = true;
975                                                }
976                                        }
977
978                                        if (!ableToHandleCode) {
979                                                failureMessage = getFailureMessageForMissingOrUnusableCodeSystem(
980                                                                includeOrExcludeSystemResource, loadedCodeSystemUrl);
981                                        }
982                                }
983
984                        } else {
985                                ableToHandleCode = true;
986                        }
987
988                        if (!ableToHandleCode) {
989                                if (failureMessage == null) {
990                                        if (includeOrExcludeSystemResource == null) {
991                                                failureMessage = getFailureMessageForMissingOrUnusableCodeSystem(
992                                                                includeOrExcludeSystemResource, loadedCodeSystemUrl);
993                                        } else {
994                                                failureMessage = "Unable to expand value set";
995                                        }
996                                }
997
998                                throw new ExpansionCouldNotBeCompletedInternallyException(
999                                                Msg.code(702) + failureMessage,
1000                                                new CodeValidationIssue(
1001                                                                failureMessage,
1002                                                                IssueSeverity.ERROR,
1003                                                                CodeValidationIssueCode.NOT_FOUND,
1004                                                                CodeValidationIssueCoding.NOT_FOUND));
1005                        }
1006
1007                        if (includeOrExcludeSystemResource != null
1008                                        && includeOrExcludeSystemResource.getContent() != Enumerations.CodeSystemContentMode.NOTPRESENT) {
1009                                addCodes(
1010                                                includeOrExcludeConceptSystemUrl,
1011                                                includeOrExcludeConceptSystemVersion,
1012                                                includeOrExcludeSystemResource.getConcept(),
1013                                                nextCodeList,
1014                                                wantCodes);
1015                        }
1016                }
1017
1018                for (CanonicalType nextValueSetInclude : theInclude.getValueSet()) {
1019                        org.hl7.fhir.r5.model.ValueSet vs = valueSetLoader.apply(nextValueSetInclude.getValueAsString());
1020                        if (vs != null) {
1021                                org.hl7.fhir.r5.model.ValueSet subExpansion = expandValueSetR5(
1022                                                                theValidationSupportContext, vs, theWantSystemUrlAndVersion, theWantCode)
1023                                                .getValueSet();
1024                                if (subExpansion == null) {
1025                                        String theMessage = "Failed to expand ValueSet: " + nextValueSetInclude.getValueAsString();
1026                                        throw new ExpansionCouldNotBeCompletedInternallyException(
1027                                                        Msg.code(703) + theMessage,
1028                                                        new CodeValidationIssue(
1029                                                                        theMessage,
1030                                                                        IssueSeverity.ERROR,
1031                                                                        CodeValidationIssueCode.INVALID,
1032                                                                        CodeValidationIssueCoding.VS_INVALID));
1033                                }
1034                                for (org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent next :
1035                                                subExpansion.getExpansion().getContains()) {
1036                                        nextCodeList.add(new FhirVersionIndependentConcept(
1037                                                        next.getSystem(), next.getCode(), next.getDisplay(), next.getVersion()));
1038                                }
1039                        }
1040                }
1041
1042                boolean retVal = false;
1043                ValueSetExpansionFilterContext valueSetExpansionFilterContext =
1044                                new ValueSetExpansionFilterContext(includeOrExcludeSystemResource, theInclude.getFilter());
1045
1046                for (FhirVersionIndependentConcept next : nextCodeList) {
1047                        if (includeOrExcludeSystemResource != null && theWantCode != null) {
1048                                boolean matches = includeOrExcludeSystemResource.getCaseSensitive()
1049                                                ? theWantCode.equals(next.getCode())
1050                                                : theWantCode.equalsIgnoreCase(next.getCode());
1051
1052                                if (!matches) {
1053                                        continue;
1054                                }
1055                        }
1056
1057                        if (!valueSetExpansionFilterContext.isFiltered(next)) {
1058                                theConsumer.accept(next);
1059                                retVal = true;
1060                        }
1061                }
1062
1063                return retVal;
1064        }
1065
1066        private Function<String, org.hl7.fhir.r5.model.ValueSet> newValueSetLoader(
1067                        ValidationSupportContext theValidationSupportContext) {
1068                return t -> {
1069                        IBaseResource valueSet =
1070                                        theValidationSupportContext.getRootValidationSupport().fetchValueSet(t);
1071                        return myVersionCanonicalizer.valueSetToValidatorCanonical(valueSet);
1072                };
1073        }
1074
1075        private Function<String, CodeSystem> newCodeSystemLoader(ValidationSupportContext theValidationSupportContext) {
1076                FhirVersionEnum version = myCtx.getVersion().getVersion();
1077                if (FhirVersionEnum.DSTU2.equals(version) || FhirVersionEnum.DSTU2_HL7ORG.equals(version)) {
1078                        return t -> {
1079                                IBaseResource codeSystem =
1080                                                theValidationSupportContext.getRootValidationSupport().fetchCodeSystem(t);
1081                                CodeSystem retVal = null;
1082                                if (codeSystem != null) {
1083                                        retVal = new CodeSystem();
1084                                        if (codeSystem instanceof ca.uhn.fhir.model.dstu2.resource.ValueSet codeSystemCasted) {
1085                                                retVal.setUrl(codeSystemCasted.getUrl());
1086                                                addCodesDstu2(codeSystemCasted.getCodeSystem().getConcept(), retVal.getConcept());
1087                                        } else {
1088                                                org.hl7.fhir.dstu2.model.ValueSet codeSystemCasted =
1089                                                                (org.hl7.fhir.dstu2.model.ValueSet) codeSystem;
1090                                                retVal.setUrl(codeSystemCasted.getUrl());
1091                                                addCodesDstu2Hl7Org(codeSystemCasted.getCodeSystem().getConcept(), retVal.getConcept());
1092                                        }
1093                                }
1094                                return retVal;
1095                        };
1096                } else {
1097                        return t -> {
1098                                IBaseResource codeSystem =
1099                                                theValidationSupportContext.getRootValidationSupport().fetchCodeSystem(t);
1100                                return myVersionCanonicalizer.codeSystemToValidatorCanonical(codeSystem);
1101                        };
1102                }
1103        }
1104
1105        private String getFailureMessageForMissingOrUnusableCodeSystem(
1106                        CodeSystem includeOrExcludeSystemResource, String loadedCodeSystemUrl) {
1107                String failureMessage;
1108                if (includeOrExcludeSystemResource == null) {
1109                        failureMessage = "Unable to expand ValueSet because CodeSystem could not be found: " + loadedCodeSystemUrl;
1110                } else {
1111                        assert includeOrExcludeSystemResource.getContent() == Enumerations.CodeSystemContentMode.NOTPRESENT;
1112                        failureMessage =
1113                                        "Unable to expand ValueSet because CodeSystem has CodeSystem.content=not-present but contents were not found: "
1114                                                        + loadedCodeSystemUrl;
1115                }
1116                return failureMessage;
1117        }
1118
1119        private void addCodes(
1120                        String theCodeSystemUrl,
1121                        String theCodeSystemVersion,
1122                        List<CodeSystem.ConceptDefinitionComponent> theSource,
1123                        List<FhirVersionIndependentConcept> theTarget,
1124                        Set<String> theCodeFilter) {
1125                for (CodeSystem.ConceptDefinitionComponent next : theSource) {
1126                        if (isNotBlank(next.getCode())) {
1127                                if (theCodeFilter == null || theCodeFilter.contains(next.getCode())) {
1128                                        theTarget.add(new FhirVersionIndependentConcept(
1129                                                        theCodeSystemUrl, next.getCode(), next.getDisplay(), theCodeSystemVersion));
1130                                }
1131                        }
1132                        addCodes(theCodeSystemUrl, theCodeSystemVersion, next.getConcept(), theTarget, theCodeFilter);
1133                }
1134        }
1135
1136        private String optionallyPopulateVersionFromUrl(String theSystemUrl, String theVersion) {
1137                if (contains(theSystemUrl, OUR_PIPE_CHARACTER) && isBlank(theVersion)) {
1138                        theVersion = substringAfter(theSystemUrl, OUR_PIPE_CHARACTER);
1139                }
1140                return theVersion;
1141        }
1142
1143        private static void populateSourceDetailsForInMemoryExpansion(
1144                        String theValueSetUrl, CodeValidationResult codeValidationResult) {
1145                codeValidationResult.setSourceDetails(
1146                                "Code was validated against in-memory expansion of ValueSet: " + theValueSetUrl);
1147        }
1148
1149        public static CodeValidationResult createResultForDisplayMismatch(
1150                        FhirContext theFhirContext,
1151                        String theCode,
1152                        String theDisplay,
1153                        String theExpectedDisplay,
1154                        String theCodeSystem,
1155                        String theCodeSystemVersion,
1156                        IssueSeverity theIssueSeverityForCodeDisplayMismatch) {
1157                return createResultForDisplayMismatch(
1158                                theFhirContext,
1159                                theCode,
1160                                theDisplay,
1161                                theExpectedDisplay,
1162                                theCodeSystem,
1163                                theCodeSystemVersion,
1164                                "",
1165                                theIssueSeverityForCodeDisplayMismatch);
1166        }
1167
1168        private static CodeValidationResult createResultForDisplayMismatch(
1169                        FhirContext theFhirContext,
1170                        String theCode,
1171                        String theDisplay,
1172                        String theExpectedDisplay,
1173                        String theCodeSystem,
1174                        String theCodeSystemVersion,
1175                        String theMessageAppend,
1176                        IssueSeverity theIssueSeverityForCodeDisplayMismatch) {
1177
1178                String message;
1179                IssueSeverity issueSeverity = theIssueSeverityForCodeDisplayMismatch;
1180                if (issueSeverity == IssueSeverity.INFORMATION) {
1181                        message = null;
1182                        issueSeverity = null;
1183                } else {
1184                        message = theFhirContext
1185                                                        .getLocalizer()
1186                                                        .getMessage(
1187                                                                        InMemoryTerminologyServerValidationSupport.class,
1188                                                                        "displayMismatch",
1189                                                                        theDisplay,
1190                                                                        theExpectedDisplay,
1191                                                                        theCodeSystem,
1192                                                                        theCode)
1193                                        + theMessageAppend;
1194                }
1195                CodeValidationResult codeValidationResult = new CodeValidationResult()
1196                                .setSeverity(issueSeverity)
1197                                .setMessage(message)
1198                                .setCode(theCode)
1199                                .setCodeSystemVersion(theCodeSystemVersion)
1200                                .setDisplay(theExpectedDisplay);
1201                if (issueSeverity != null) {
1202                        codeValidationResult.setIssues(Collections.singletonList(new CodeValidationIssue(
1203                                        message,
1204                                        theIssueSeverityForCodeDisplayMismatch,
1205                                        CodeValidationIssueCode.INVALID,
1206                                        CodeValidationIssueCoding.INVALID_DISPLAY)));
1207                }
1208
1209                return codeValidationResult;
1210        }
1211
1212        private static void flattenAndConvertCodesDstu2(
1213                        List<ca.uhn.fhir.model.dstu2.resource.ValueSet.ExpansionContains> theInput,
1214                        List<FhirVersionIndependentConcept> theFhirVersionIndependentConcepts) {
1215                for (ca.uhn.fhir.model.dstu2.resource.ValueSet.ExpansionContains next : theInput) {
1216                        theFhirVersionIndependentConcepts.add(
1217                                        new FhirVersionIndependentConcept(next.getSystem(), next.getCode(), next.getDisplay()));
1218                        flattenAndConvertCodesDstu2(next.getContains(), theFhirVersionIndependentConcepts);
1219                }
1220        }
1221
1222        private static void flattenAndConvertCodesDstu2Hl7Org(
1223                        List<org.hl7.fhir.dstu2.model.ValueSet.ValueSetExpansionContainsComponent> theInput,
1224                        List<FhirVersionIndependentConcept> theFhirVersionIndependentConcepts) {
1225                for (org.hl7.fhir.dstu2.model.ValueSet.ValueSetExpansionContainsComponent next : theInput) {
1226                        theFhirVersionIndependentConcepts.add(
1227                                        new FhirVersionIndependentConcept(next.getSystem(), next.getCode(), next.getDisplay()));
1228                        flattenAndConvertCodesDstu2Hl7Org(next.getContains(), theFhirVersionIndependentConcepts);
1229                }
1230        }
1231
1232        private static void flattenAndConvertCodesDstu3(
1233                        List<org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent> theInput,
1234                        List<FhirVersionIndependentConcept> theFhirVersionIndependentConcepts) {
1235                for (org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent next : theInput) {
1236                        theFhirVersionIndependentConcepts.add(new FhirVersionIndependentConcept(
1237                                        next.getSystem(), next.getCode(), next.getDisplay(), next.getVersion()));
1238                        flattenAndConvertCodesDstu3(next.getContains(), theFhirVersionIndependentConcepts);
1239                }
1240        }
1241
1242        private static void flattenAndConvertCodesR4(
1243                        List<org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent> theInput,
1244                        List<FhirVersionIndependentConcept> theFhirVersionIndependentConcepts) {
1245                for (org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent next : theInput) {
1246                        theFhirVersionIndependentConcepts.add(new FhirVersionIndependentConcept(
1247                                        next.getSystem(), next.getCode(), next.getDisplay(), next.getVersion()));
1248                        flattenAndConvertCodesR4(next.getContains(), theFhirVersionIndependentConcepts);
1249                }
1250        }
1251
1252        private static void flattenAndConvertCodesR4B(
1253                        List<org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionContainsComponent> theInput,
1254                        List<FhirVersionIndependentConcept> theFhirVersionIndependentConcepts) {
1255                for (org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionContainsComponent next : theInput) {
1256                        theFhirVersionIndependentConcepts.add(new FhirVersionIndependentConcept(
1257                                        next.getSystem(), next.getCode(), next.getDisplay(), next.getVersion()));
1258                        flattenAndConvertCodesR4B(next.getContains(), theFhirVersionIndependentConcepts);
1259                }
1260        }
1261
1262        private static void flattenAndConvertCodesR5(
1263                        List<org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent> theInput,
1264                        List<FhirVersionIndependentConcept> theFhirVersionIndependentConcepts) {
1265                for (org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent next : theInput) {
1266                        theFhirVersionIndependentConcepts.add(new FhirVersionIndependentConcept(
1267                                        next.getSystem(), next.getCode(), next.getDisplay(), next.getVersion()));
1268                        flattenAndConvertCodesR5(next.getContains(), theFhirVersionIndependentConcepts);
1269                }
1270        }
1271
1272        public static class ExpansionCouldNotBeCompletedInternallyException extends Exception {
1273
1274                private static final long serialVersionUID = -2226561628771483085L;
1275                private final CodeValidationIssue myCodeValidationIssue;
1276
1277                public ExpansionCouldNotBeCompletedInternallyException(
1278                                String theMessage, CodeValidationIssue theCodeValidationIssue) {
1279                        super(theMessage);
1280                        myCodeValidationIssue = theCodeValidationIssue;
1281                }
1282
1283                public CodeValidationIssue getCodeValidationIssue() {
1284                        return myCodeValidationIssue;
1285                }
1286        }
1287
1288        private static class ValueSetAndMessages {
1289
1290                private org.hl7.fhir.r5.model.ValueSet myValueSet;
1291                private List<String> myMessages = new ArrayList<>();
1292
1293                public void setValueSet(org.hl7.fhir.r5.model.ValueSet theValueSet) {
1294                        myValueSet = theValueSet;
1295                }
1296
1297                public void addMessage(String theMessage) {
1298                        myMessages.add(theMessage);
1299                }
1300
1301                public org.hl7.fhir.r5.model.ValueSet getValueSet() {
1302                        return myValueSet;
1303                }
1304
1305                public List<String> getMessages() {
1306                        return myMessages;
1307                }
1308        }
1309}