diff --git a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRCodeSystemVersion.java b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRCodeSystemVersion.java index 8116ee9f3..c39658de6 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRCodeSystemVersion.java +++ b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRCodeSystemVersion.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Extension; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; @@ -16,6 +17,7 @@ import org.springframework.data.elasticsearch.annotations.*; import java.util.Date; +import java.util.List; import static org.snomed.snowstorm.fhir.config.FHIRConstants.*; @@ -31,6 +33,9 @@ public class FHIRCodeSystemVersion { @Field(type = FieldType.Keyword) private String version; + @Field(type = FieldType.Keyword) + private String language; + @Field(type = FieldType.Date, format = DateFormat.date_time) private Date date; @@ -55,6 +60,9 @@ public class FHIRCodeSystemVersion { @Field(type = FieldType.Keyword) private String content; + @Field(type = FieldType.Nested) + private List<Extension> extensions; + @Transient private String snomedBranch; @@ -84,6 +92,7 @@ public FHIRCodeSystemVersion(CodeSystem codeSystem) { version = codeSystem.getVersion(); date = codeSystem.getDate(); title = codeSystem.getTitle(); + language = codeSystem.getLanguage(); if (title != null) { title = title.replace(" Code System", ""); } @@ -96,6 +105,7 @@ public FHIRCodeSystemVersion(CodeSystem codeSystem) { compositional = codeSystem.getCompositional(); CodeSystem.CodeSystemContentMode codeSystemContent = codeSystem.getContent(); content = codeSystemContent != null ? codeSystemContent.toCode() : null; + extensions = codeSystem.getExtension(); } public FHIRCodeSystemVersion(CodeSystemVersion snomedVersion) { @@ -144,9 +154,13 @@ public FHIRCodeSystemVersion(org.snomed.snowstorm.core.data.domain.CodeSystem sn public CodeSystem toHapiCodeSystem() { CodeSystem codeSystem = new CodeSystem(); + codeSystem.setExtension(extensions); codeSystem.setId(id); codeSystem.setUrl(url); - codeSystem.setVersion(version); + if(!"0".equals(version)) { + codeSystem.setVersion(version); + } + codeSystem.setLanguage(language); codeSystem.setDate(date); codeSystem.setName(name); codeSystem.setTitle(title); @@ -181,7 +195,11 @@ public boolean isVersionMatch(String requestedVersion) { } public String getCanonical() { - return url + "|" + version; + if ("0".equals(version)){ + return url; + } else { + return url + "|" + version; + } } public String getId() { @@ -248,6 +266,14 @@ public void setPublisher(String publisher) { this.publisher = publisher; } + public List<Extension> getExtensions() { + return extensions; + } + + public void setExtensions(List<Extension> extensions) { + this.extensions = extensions; + } + public String getHierarchyMeaning() { return hierarchyMeaning; } @@ -291,4 +317,12 @@ public CodeSystemVersion getSnomedCodeSystemVersion() { public String toString() { return getId(); } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } } diff --git a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRConcept.java b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRConcept.java index 7ea2ec550..09ebd33ba 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRConcept.java +++ b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRConcept.java @@ -4,7 +4,7 @@ import ca.uhn.fhir.jpa.entity.TermConceptDesignation; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink; import ca.uhn.fhir.jpa.entity.TermConceptProperty; -import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.*; import org.snomed.snowstorm.core.data.domain.ConceptMini; import org.snomed.snowstorm.core.data.domain.Description; import org.snomed.snowstorm.core.pojo.LanguageDialect; @@ -24,6 +24,8 @@ @Document(indexName = "#{@indexNameProvider.indexName('fhir-concept')}", createIndex = false) public class FHIRConcept implements FHIRGraphNode { + public static final String EXTENSION_MARKER = "://"; + public interface Fields { String CODE_SYSTEM_VERSION = "codeSystemVersion"; @@ -33,6 +35,7 @@ public interface Fields { String PARENTS = "parents"; String ANCESTORS = "ancestors"; String PROPERTIES = "properties"; + String EXTENSIONS = "extensions"; } @Id // Internal ID @@ -62,6 +65,8 @@ public interface Fields { private Map<String, List<FHIRProperty>> properties; + private Map<String, List<FHIRProperty>> extensions; + public FHIRConcept() { active = true; } @@ -80,11 +85,17 @@ public FHIRConcept(TermConcept termConcept, FHIRCodeSystemVersion codeSystemVers } properties = new HashMap<>(); - for (TermConceptProperty property : termConcept.getProperties()) { + for (TermConceptProperty property : termConcept.getProperties().stream().filter(p -> !p.getKey().contains(EXTENSION_MARKER)).toList()) { properties.computeIfAbsent(property.getKey(), (i) -> new ArrayList<>()) .add(new FHIRProperty(property)); } + extensions = new HashMap<>(); + for (TermConceptProperty extension : termConcept.getProperties().stream().filter(p -> p.getKey().contains(EXTENSION_MARKER)).toList()) { + extensions.computeIfAbsent(extension.getKey(), (i) -> new ArrayList<>()) + .add(new FHIRProperty(extension)); + } + parents = new HashSet<>(); for (TermConceptParentChildLink parent : termConcept.getParents()) { parents.add(parent.getParent().getCode()); @@ -109,9 +120,37 @@ public FHIRConcept(CodeSystem.ConceptDefinitionComponent definitionConcept, FHIR Optional.ofNullable(definitionConcept.getDefinition()).ifPresent(x -> properties.put("definition",Collections.singletonList(new FHIRProperty("definition",null,x,FHIRProperty.STRING)))); definitionConcept.getProperty().stream().filter(x-> x.getCode().equals("status") && x.getValueCodeType().getCode().equals("retired") ).findFirst().ifPresentOrElse(x -> active= false, ()->active =true); properties.put("inactive",Collections.singletonList(new FHIRProperty("inactive",null,Boolean.toString(!isActive()),FHIRProperty.BOOLEAN))); + extensions = new HashMap<>(); + definitionConcept.getExtension().forEach( + e ->{ + Optional.ofNullable(extensions.get(e.getUrl())).ifPresentOrElse(list ->{ + list.add(new FHIRProperty(e.getUrl(), null,e.getValue().primitiveValue(), FHIRProperty.typeToFHIRPropertyType(e.getValue()))); + + }, ()->{ + List<FHIRProperty> list = new ArrayList<>(); + list.add(new FHIRProperty(e.getUrl(), null,e.getValue().primitiveValue(), FHIRProperty.typeToFHIRPropertyType(e.getValue()))); + extensions.put(e.getUrl(), list); + }); + + } + ); parents = new HashSet<>(); for (CodeSystem.ConceptPropertyComponent propertyComponent : definitionConcept.getProperty()) { - properties.computeIfAbsent(propertyComponent.getCode(), k -> new ArrayList<>()).add(new FHIRProperty(propertyComponent)); + if (properties.get(propertyComponent.getCode())==null && !propertyComponent.getCode().contains(EXTENSION_MARKER)){ + properties.put(propertyComponent.getCode(),new ArrayList<>()); + } + try{ + if(!propertyComponent.getCode().contains(EXTENSION_MARKER)){ + properties.get(propertyComponent.getCode()).add(new FHIRProperty(propertyComponent)); + } + } catch( UnsupportedOperationException e){ + List<FHIRProperty> unmodifiableList = properties.get(propertyComponent.getCode()); + List<FHIRProperty> modifiableList = new ArrayList<>(); + modifiableList.addAll(unmodifiableList); + properties.put(propertyComponent.getCode(), modifiableList); + properties.get(propertyComponent.getCode()).add(new FHIRProperty(propertyComponent)); + } + if (propertyComponent.getCode().equals("parent") || propertyComponent.getCode().equals("subsumedBy")) { parents.add(propertyComponent.hasValueCoding() ? propertyComponent.getValueCoding().getCode() : propertyComponent.getValue().toString()); } @@ -228,4 +267,12 @@ public Map<String, List<FHIRProperty>> getProperties() { public void setProperties(Map<String, List<FHIRProperty>> properties) { this.properties = properties; } + + public Map<String, List<FHIRProperty>> getExtensions() { + return extensions; + } + + public void setExtensions(Map<String, List<FHIRProperty>> extensions) { + this.extensions = extensions; + } } diff --git a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRDesignation.java b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRDesignation.java index ffcf08098..58e212116 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRDesignation.java +++ b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRDesignation.java @@ -1,10 +1,17 @@ package org.snomed.snowstorm.fhir.domain; import ca.uhn.fhir.jpa.entity.TermConceptDesignation; +import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.ValueSet; import org.snomed.snowstorm.core.data.domain.Description; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + import static org.snomed.snowstorm.fhir.config.FHIRConstants.SNOMED_URI; public class FHIRDesignation { @@ -13,6 +20,10 @@ public class FHIRDesignation { private String use; private String value; + + + private List<FHIRExtension> extensions; + public FHIRDesignation() { } @@ -43,6 +54,24 @@ public FHIRDesignation(CodeSystem.ConceptDefinitionDesignationComponent designat language = designation.getLanguage(); value = designation.getValue(); setUse(designation.getUse()); + designation.getExtension().forEach( ext -> { + if (extensions == null){ + extensions = new ArrayList<>(); + } + extensions.add(new FHIRExtension(ext)); + }); + } + + public FHIRDesignation(ValueSet.ConceptReferenceDesignationComponent designation) { + language = designation.getLanguage(); + value = designation.getValue(); + setUse(designation.getUse()); + designation.getExtension().forEach( ext -> { + if (extensions == null){ + extensions = new ArrayList<>(); + } + extensions.add(new FHIRExtension(ext)); + }); } public void setUse(Coding useCoding) { @@ -50,7 +79,11 @@ public void setUse(Coding useCoding) { } public void setUse(String useSystem, String useCode) { - use = useSystem + "|" + useCode; + if(useSystem == null && useCode == null){ + use = null; + } else { + use = useSystem + "|" + useCode; + } } public Coding getUseCoding() { @@ -65,6 +98,17 @@ public Coding getUseCoding() { return null; } + public ValueSet.ConceptReferenceDesignationComponent getHapi() { + ValueSet.ConceptReferenceDesignationComponent hapiConceptReferenceDesignationComponent = new ValueSet.ConceptReferenceDesignationComponent(); + hapiConceptReferenceDesignationComponent.setLanguage(language); + hapiConceptReferenceDesignationComponent.setValue(value); + if (StringUtils.isNotEmpty(use)) { + hapiConceptReferenceDesignationComponent.setUse(this.getUseCoding()); + } + hapiConceptReferenceDesignationComponent.setExtension(Optional.ofNullable(extensions).orElse(Collections.emptyList()).stream().map(d->d.getHapi()).toList()); + return hapiConceptReferenceDesignationComponent; + } + private static Coding addKnownDisplays(Coding coding) { if (coding != null) { if (SNOMED_URI.equals(coding.getSystem())) { @@ -107,4 +151,12 @@ public String getValue() { public void setValue(String value) { this.value = value; } + + public List<FHIRExtension> getExtensions() { + return extensions; + } + + public void setExtensions(List<FHIRExtension> extensions) { + this.extensions = extensions; + } } diff --git a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRExtension.java b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRExtension.java new file mode 100644 index 000000000..d798fb878 --- /dev/null +++ b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRExtension.java @@ -0,0 +1,93 @@ +package org.snomed.snowstorm.fhir.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.hl7.fhir.r4.model.*; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.snomed.snowstorm.core.util.CollectionUtils.orEmpty; + +public class FHIRExtension { + + @Field(type = FieldType.Keyword) + private String uri; + @Field(type = FieldType.Keyword) + private String value; + @Field(type = FieldType.Keyword) + private String type; + + private List<FHIRExtension> extensions; + + public FHIRExtension() { + } + + public FHIRExtension(Extension hapiExtension) { + uri = hapiExtension.getUrl(); + if (hapiExtension.getValue()!= null) { + value = hapiExtension.getValue().primitiveValue(); + type = hapiExtension.getValue().fhirType(); + } else { + hapiExtension.getExtension().forEach( extension -> { + if (extensions == null){ + extensions = new ArrayList<>(); + } + extensions.add(new FHIRExtension(extension)); + }); + } + } + + @JsonIgnore + public Extension getHapi() { + Extension extension = new Extension(); + extension.setUrl(uri); + Optional.ofNullable(getType(value, type)) + .ifPresentOrElse(extension::setValue, () ->{ + orEmpty(extensions).forEach( fhirExtension -> { + extension.addExtension(fhirExtension.getHapi()); + }); + + }); + return extension; + } + public static Type getType(String primitiveValue, String fhirType){ + if (fhirType == null) return null; + return switch (fhirType) { + case "integer" -> new IntegerType(primitiveValue); + case "boolean" -> new BooleanType(primitiveValue); + case "string" -> new StringType(primitiveValue); + case "decimal" -> new DecimalType(primitiveValue); + case "id" -> new IdType(primitiveValue); + case "canonical" -> new CanonicalType(primitiveValue); + case "code" -> new CodeType(primitiveValue); + default -> null; + }; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public List<FHIRExtension> getExtensions() { + return extensions; + } + + public void setExtensions(List<FHIRExtension> extensions) { + this.extensions = extensions; + } +} diff --git a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRProperty.java b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRProperty.java index 321b2c377..045cc9545 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRProperty.java +++ b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRProperty.java @@ -1,15 +1,23 @@ package org.snomed.snowstorm.fhir.domain; +import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry; import ca.uhn.fhir.jpa.entity.TermConceptProperty; import ca.uhn.fhir.jpa.entity.TermConceptPropertyTypeEnum; import org.hl7.fhir.r4.model.*; +import java.util.Arrays; + public class FHIRProperty { public static final String STRING = "STRING"; public static final String CODING = "CODING"; public static final String CODE = "CODE"; public static final String BOOLEAN = "BOOLEAN"; + public static final String INTEGER = "INTEGER"; + public static final String DECIMAL = "DECIMAL"; + public static final String[] URLS = {"http://hl7.org/fhir/StructureDefinition/itemWeight", + "http://hl7.org/fhir/StructureDefinition/codesystem-label", + "http://hl7.org/fhir/StructureDefinition/codesystem-conceptOrder"}; private String code; private String display; @@ -50,7 +58,33 @@ public FHIRProperty(CodeSystem.ConceptPropertyComponent propertyComponent) { } else if (propertyComponent.hasValueBooleanType()){ value = propertyComponent.getValueBooleanType().getValueAsString(); type = BOOLEAN; + } else if (propertyComponent.hasValueIntegerType()){ + value = propertyComponent.getValueIntegerType().getValueAsString(); + type = INTEGER; + } else if (propertyComponent.hasValueDecimalType()){ + value = propertyComponent.getValueDecimalType().getValueAsString(); + type = DECIMAL; + } + } + + static String typeToFHIRPropertyType(Type value) { + String fhirPropertyType; + if (value instanceof CodeType) { + fhirPropertyType = CODE; + } else if (value instanceof StringType){ + fhirPropertyType = STRING; + } else if (value instanceof Coding) { + fhirPropertyType = CODING; + } else if (value instanceof BooleanType) { + fhirPropertyType = BOOLEAN; + } else if (value instanceof IntegerType) { + fhirPropertyType = INTEGER; + } else if (value instanceof DecimalType) { + fhirPropertyType = DECIMAL; + }else { + throw new RuntimeException("unknown FHIRProperty type"); } + return fhirPropertyType; } public Type toHapiValue(String systemVersionUrl) { @@ -62,6 +96,10 @@ public Type toHapiValue(String systemVersionUrl) { return new Coding(systemVersionUrl, value, display); } else if (BOOLEAN.equals(type)) { return new BooleanType(value); + } else if (INTEGER.equals(type)) { + return new IntegerType(value); + }else if (DECIMAL.equals(type)) { + return new DecimalType(value); } return null; } @@ -89,4 +127,8 @@ public String getValue() { public void setValue(String value) { this.value = value; } + + public boolean isSpecialExtension() { + return Arrays.asList(URLS).contains(code); + } } diff --git a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSet.java b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSet.java index 6ee66ed11..2da690e58 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSet.java +++ b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSet.java @@ -13,7 +13,9 @@ import org.springframework.data.elasticsearch.annotations.Setting; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Optional; import static org.snomed.snowstorm.core.util.CollectionUtils.orEmpty; @@ -26,6 +28,9 @@ public class FHIRValueSet { @Field(type = FieldType.Keyword) private String url; + @Field(type = FieldType.Keyword) + private String language; + private List<FHIRIdentifier> identifier; @Field(type = FieldType.Keyword) @@ -58,6 +63,8 @@ public class FHIRValueSet { private FHIRValueSetCompose compose; + private List<FHIRExtension> extensions; + public FHIRValueSet() { } @@ -71,6 +78,7 @@ public FHIRValueSet(ValueSet hapiValueSet) { } identifier.add(new FHIRIdentifier(hapiIdentifier)); } + language = hapiValueSet.getLanguage(); version = hapiValueSet.getVersion(); name = hapiValueSet.getName(); title = hapiValueSet.getTitle(); @@ -88,6 +96,15 @@ public FHIRValueSet(ValueSet hapiValueSet) { copyright = hapiValueSet.getCopyright(); compose = new FHIRValueSetCompose(hapiValueSet.getCompose()); + + hapiValueSet.getExtension().forEach( e -> { + if(extensions == null){ + extensions = new ArrayList<>(); + } + + extensions.add(new FHIRExtension(e)); + + }); } @JsonIgnore @@ -95,6 +112,7 @@ public ValueSet getHapi() { ValueSet valueSet = new ValueSet(); valueSet.setId(id); valueSet.setUrl(url); + valueSet.setLanguage(language); for (FHIRIdentifier fhirIdentifier : orEmpty(getIdentifier())) { valueSet.addIdentifier(fhirIdentifier.getHapi()); @@ -115,6 +133,10 @@ public ValueSet getHapi() { valueSet.setCopyright(copyright); valueSet.setCompose(compose.getHapi()); + + Optional.ofNullable(extensions).orElse(Collections.emptyList()).forEach( fe -> { + valueSet.addExtension(fe.getHapi()); + }); return valueSet; } @@ -229,4 +251,20 @@ public FHIRValueSetCompose getCompose() { public void setCompose(FHIRValueSetCompose compose) { this.compose = compose; } + + public List<FHIRExtension> getExtensions() { + return extensions; + } + + public void setExtensions(List<FHIRExtension> extensions) { + this.extensions = extensions; + } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } } diff --git a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSetCompose.java b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSetCompose.java index b6447fa33..f27501b51 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSetCompose.java +++ b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSetCompose.java @@ -12,6 +12,9 @@ public class FHIRValueSetCompose { private List<FHIRValueSetCriteria> include; private List<FHIRValueSetCriteria> exclude; + + private List<FHIRExtension> extensions; + private Boolean inactive; public FHIRValueSetCompose() { @@ -30,6 +33,15 @@ public FHIRValueSetCompose(ValueSet.ValueSetComposeComponent hapiCompose) { for (ValueSet.ConceptSetComponent hapiExclude : hapiCompose.getExclude()) { addInclude(new FHIRValueSetCriteria(hapiExclude)); } + + hapiCompose.getExtension().forEach( ext -> { + if (extensions == null){ + extensions = new ArrayList<>(); + } + + extensions.add(new FHIRExtension(ext)); + + }); } public ValueSet.ValueSetComposeComponent getHapi() { @@ -43,6 +55,10 @@ public ValueSet.ValueSetComposeComponent getHapi() { for (FHIRValueSetCriteria exclude : orEmpty(getExclude())) { hapiCompose.addExclude(exclude.getHapi()); } + + orEmpty(extensions).forEach( ext ->{ + hapiCompose.addExtension(ext.getHapi()); + }); return hapiCompose; } @@ -83,4 +99,13 @@ public void setInactive(Boolean inactive) { public Boolean isInactive() { return inactive; } + + public List<FHIRExtension> getExtensions() { + return extensions; + } + + public void setExtensions(List<FHIRExtension> extensions) { + this.extensions = extensions; + } + } diff --git a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSetCriteria.java b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSetCriteria.java index 3fe4851d1..a7f773678 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSetCriteria.java +++ b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSetCriteria.java @@ -6,7 +6,9 @@ import org.springframework.data.elasticsearch.annotations.FieldType; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import static org.snomed.snowstorm.core.util.CollectionUtils.orEmpty; @@ -22,6 +24,8 @@ public class FHIRValueSetCriteria { @Field(type = FieldType.Keyword) private List<String> codes; + private List<FHIRValueSetCriteriaConcept> conceptReferences; + private List<FHIRValueSetFilter> filter; private List<String> valueSet; @@ -38,6 +42,13 @@ public FHIRValueSetCriteria(ValueSet.ConceptSetComponent hapiCriteria) { } codes.add(code.getCode()); } + hapiCriteria.getConcept().stream().forEach( c ->{ + if (conceptReferences == null){ + conceptReferences = new ArrayList<>(); + + } + conceptReferences.add(new FHIRValueSetCriteriaConcept(c)); + }); for (ValueSet.ConceptSetFilterComponent hapiFilter : hapiCriteria.getFilter()) { if (filter == null) { filter = new ArrayList<>(); @@ -54,6 +65,12 @@ public ValueSet.ConceptSetComponent getHapi() { for (String code : orEmpty(codes)) { ValueSet.ConceptReferenceComponent component = new ValueSet.ConceptReferenceComponent(); component.setCode(code); + conceptReferences.stream().filter(x -> code.equals(x.getCode())) + .forEach(x -> { + Optional.ofNullable(x.getExtensions()).orElse(Collections.emptyList()).forEach(ext ->component.addExtension(ext.getHapi())); + Optional.ofNullable(x.getDesignations()).orElse(Collections.emptyList()).forEach(d ->component.addDesignation(d.getHapi())); + } + ); hapiConceptSet.addConcept(component); } for (FHIRValueSetFilter filter : orEmpty(getFilter())) { diff --git a/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSetCriteriaConcept.java b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSetCriteriaConcept.java new file mode 100644 index 000000000..530541b01 --- /dev/null +++ b/src/main/java/org/snomed/snowstorm/fhir/domain/FHIRValueSetCriteriaConcept.java @@ -0,0 +1,82 @@ +package org.snomed.snowstorm.fhir.domain; + +import org.hl7.fhir.r4.model.CanonicalType; +import org.hl7.fhir.r4.model.ValueSet; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.snomed.snowstorm.core.util.CollectionUtils.orEmpty; + +public class FHIRValueSetCriteriaConcept { + + @Field(type = FieldType.Keyword) + private String code; + + + + private List<FHIRExtension> extensions; + + + + private List<FHIRDesignation> designations; + + + + public FHIRValueSetCriteriaConcept() { + } + + public FHIRValueSetCriteriaConcept(ValueSet.ConceptReferenceComponent hapiConceptReferenceComponent) { + code = hapiConceptReferenceComponent.getCode(); + hapiConceptReferenceComponent.getExtension().forEach( ext -> { + if (extensions == null){ + extensions = new ArrayList<>(); + } + extensions.add(new FHIRExtension(ext)); + }); + hapiConceptReferenceComponent.getDesignation().forEach( d -> { + if (designations == null){ + designations = new ArrayList<>(); + } + designations.add(new FHIRDesignation(d)); + }); + } + + public ValueSet.ConceptReferenceComponent getHapi() { + ValueSet.ConceptReferenceComponent hapiConceptReferenceComponent = new ValueSet.ConceptReferenceComponent(); + hapiConceptReferenceComponent.setCode(code); + hapiConceptReferenceComponent.setExtension(Optional.ofNullable(extensions).orElse(Collections.emptyList()).stream().map(e->e.getHapi()).toList()); + hapiConceptReferenceComponent.setDesignation(Optional.ofNullable(designations).orElse(Collections.emptyList()).stream().map(d->d.getHapi()).toList()); + return hapiConceptReferenceComponent; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public List<FHIRExtension> getExtensions() { + return extensions; + } + + public void setExtensions(List<FHIRExtension> extensions) { + this.extensions = extensions; + } + + public List<FHIRDesignation> getDesignations() { + return designations; + } + + public void setDesignations(List<FHIRDesignation> designations) { + this.designations = designations; + } + +} diff --git a/src/main/java/org/snomed/snowstorm/fhir/pojo/ValueSetExpansionParameters.java b/src/main/java/org/snomed/snowstorm/fhir/pojo/ValueSetExpansionParameters.java index f746c4cfe..849723f67 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/pojo/ValueSetExpansionParameters.java +++ b/src/main/java/org/snomed/snowstorm/fhir/pojo/ValueSetExpansionParameters.java @@ -38,16 +38,17 @@ public final class ValueSetExpansionParameters { private final CanonicalUri forceSystemVersion; private final String version; private final ValueSet valueSet; + private final String property; public ValueSetExpansionParameters(ValueSet valueSet, boolean includeDefinition1) { this(null, valueSet, null, null, null, null, null, null, null, null, null, - null, includeDefinition1, null, null, null, null, null, null, null, null, null, null); + null, includeDefinition1, null, null, null, null, null, null, null, null, null, null, null); } public ValueSetExpansionParameters(String id, ValueSet valueSet, URI url, String valueSetVersion, String context, String contextDirection, String filter, String date, Integer offset, Integer count, Boolean includeDesignations, List<String> designations, Boolean includeDefinition, Boolean activeOnly, Boolean excludeNested, Boolean excludeNotForUI, Boolean excludePostCoordinated, String displayLanguage, CanonicalUri excludeSystem, CanonicalUri systemVersion, - CanonicalUri checkSystemVersion, CanonicalUri forceSystemVersion, String version) { + CanonicalUri checkSystemVersion, CanonicalUri forceSystemVersion, String version, String property) { this.id = id; this.url = url; @@ -72,6 +73,7 @@ public ValueSetExpansionParameters(String id, ValueSet valueSet, URI url, String this.forceSystemVersion = forceSystemVersion; this.version = version; this.valueSet = valueSet; + this.property = property; } public PageRequest getPageRequest(Sort sort) { @@ -169,6 +171,10 @@ public CanonicalUri getForceSystemVersion() { return forceSystemVersion; } + public String getProperty(){ + return property; + } + public String getVersion() { return version; } diff --git a/src/main/java/org/snomed/snowstorm/fhir/services/FHIRCodeSystemService.java b/src/main/java/org/snomed/snowstorm/fhir/services/FHIRCodeSystemService.java index 31a9caedb..2ab5465bb 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/services/FHIRCodeSystemService.java +++ b/src/main/java/org/snomed/snowstorm/fhir/services/FHIRCodeSystemService.java @@ -1,6 +1,9 @@ package org.snomed.snowstorm.fhir.services; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.OperationOutcome; import org.ihtsdo.drools.helper.IdentifierHelper; import org.jetbrains.annotations.NotNull; @@ -25,6 +28,7 @@ import org.snomed.snowstorm.core.data.services.postcoordination.model.PostCoordinatedExpression; import org.snomed.snowstorm.core.pojo.LanguageDialect; import org.snomed.snowstorm.fhir.domain.FHIRCodeSystemVersion; +import org.snomed.snowstorm.fhir.domain.FHIRConcept; import org.snomed.snowstorm.fhir.domain.SubsumesResult; import org.snomed.snowstorm.fhir.pojo.CanonicalUri; import org.snomed.snowstorm.fhir.pojo.ConceptAndSystemResult; @@ -32,6 +36,7 @@ import org.snomed.snowstorm.fhir.repositories.FHIRCodeSystemRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; @@ -44,6 +49,7 @@ public class FHIRCodeSystemService { public static final String SCT_ID_PREFIX = "sct_"; + private static final int PAGESIZE = 1_000; @Autowired private FHIRCodeSystemRepository codeSystemRepository; @@ -82,83 +88,7 @@ public FHIRCodeSystemVersion createUpdate(CodeSystem codeSystem) throws ServiceE } if (codeSystem.getContent() == CodeSystem.CodeSystemContentMode.SUPPLEMENT) { - // Attempt to process SNOMED CT code system supplement / expression repository - - /* - * Validation - */ - // if not SNOMED - if (!FHIRHelper.isSnomedUri(fhirCodeSystemVersion.getUrl())) { - throw exception("At this time this server only supports CodeSystem supplements that supplement SNOMED CT and contain postcoordinated expressions.", - OperationOutcome.IssueType.NOTSUPPORTED, 400); - } - // if no dependency - if (!codeSystem.hasSupplements()) { - throw exception("SNOMED CT CodeSystem supplements must declare which SNOMED CT edition and version they supplement " + - "using the 'supplements' property.", OperationOutcome.IssueType.INVARIANT, 400); - } - // Check dependency is SNOMED - FHIRCodeSystemVersionParams dependencyParams = FHIRHelper.getCodeSystemVersionParams(CanonicalUri.fromString(codeSystem.getSupplements())); - if (!dependencyParams.isSnomed() || dependencyParams.isUnversionedSnomed() - || dependencyParams.isUnspecifiedReleasedSnomed() || dependencyParams.getVersion() == null) { - throw exception(format("The CodeSystem supplement must be a canonical URL with the SNOMED CT code system and a version using the SNOMED CT URI standard to " + - "quote a specific version of a specific edition. For example: http://snomed.info/sct|http://snomed.info/sct/900000000000207008/version/%s0131", - new GregorianCalendar().get(Calendar.YEAR)), OperationOutcome.IssueType.INVARIANT, 400); - } - // Check dependency exists - FHIRCodeSystemVersion dependantVersion = getSnomedVersionOrThrow(dependencyParams); - - // Does the requested code system already exist? - FHIRCodeSystemVersionParams existingCodeSystemParams = FHIRHelper.getCodeSystemVersionParams(codeSystem.getUrl(), codeSystem.getVersion()); - String snomedModule = existingCodeSystemParams.getSnomedModule(); - if (!IdentifierHelper.isConceptId(snomedModule) || snomedModule.length() < 10) {// Long format concept identifier including namespace. - Long suggestedModuleConceptId = null; - if (snomedModule != null && snomedModule.length() == 7) { - // The module id is actually a SNOMED CT namespace... generate concept id that could be used - int namespace = Integer.parseInt(snomedModule); - try { - List<Long> conceptIds = identifierSource.reserveIds(namespace, IdentifierService.EXTENSION_CONCEPT_PARTITION_ID, 1); - if (!conceptIds.isEmpty()) { - suggestedModuleConceptId = conceptIds.get(0); - } - } catch (ServiceException e) { - logger.warn("Failed to generate a concept id using assumed namespace '{}'", namespace, e); - } - } - if (suggestedModuleConceptId != null) { - throw exception(format("The URL of this SNOMED CT CodeSystem supplement must have a version that follows the SNOMED CT URI standard and includes a module id." + - " If a namespace was given in the version URL then the module id '%s' could be used." + - " This id has been generated using the namespace given and is the next id sequence, considering all content currently loaded into Snowstorm.", - suggestedModuleConceptId), - OperationOutcome.IssueType.INVARIANT, 400); - } else { - throw exception("The URL of this SNOMED CT CodeSystem supplement must have a version that follows the SNOMED CT URI standard and includes a module id.", - OperationOutcome.IssueType.INVARIANT, 400); - } - } - org.snomed.snowstorm.core.data.domain.CodeSystem existingCodeSystem = snomedCodeSystemService.findByUriModule(snomedModule); - if (existingCodeSystem != null) { - throw exception("A code system supplement with the same URL and version already exists. Updating SNOMED CT code system supplements is not yet supported.", - OperationOutcome.IssueType.NOTSUPPORTED, 400); - } - - org.snomed.snowstorm.core.data.domain.CodeSystem newCodeSystem = new org.snomed.snowstorm.core.data.domain.CodeSystem(); - org.snomed.snowstorm.core.data.domain.CodeSystem dependentCodeSystem = dependantVersion.getSnomedCodeSystem(); - String shortName = dependentCodeSystem.getShortName() + "-EXP"; - newCodeSystem.setShortName(shortName); - - // Append 2,3,4 etc to the short name to ensure uniqueness - int a = 2; - while (snomedCodeSystemService.find(newCodeSystem.getShortName()) != null) { - newCodeSystem.setShortName(shortName + a); - a++; - } - newCodeSystem.setName("SNOMED CT Postcoordinated Expression Repository"); - newCodeSystem.setBranchPath(String.join("/", dependentCodeSystem.getBranchPath(), newCodeSystem.getShortName())); - newCodeSystem.setUriModuleId(snomedModule); - newCodeSystem.setMaximumPostcoordinationLevel(maxPostcoordinationLevel); - org.snomed.snowstorm.core.data.domain.CodeSystem savedCodeSystem = snomedCodeSystemService.createCodeSystem(newCodeSystem); - return new FHIRCodeSystemVersion(savedCodeSystem); + return handleSupplement(codeSystem, fhirCodeSystemVersion); } else {// Not Supplement @@ -196,6 +126,211 @@ public FHIRCodeSystemVersion createUpdate(CodeSystem codeSystem) throws ServiceE } } + private @NotNull FHIRCodeSystemVersion handleSupplement(CodeSystem codeSystem, FHIRCodeSystemVersion fhirCodeSystemVersion) throws ServiceException { + // Attempt to process SNOMED CT code system supplement / expression repository + + /* + * Validation + */ + // if not SNOMED + if (!FHIRHelper.isSnomedUri(fhirCodeSystemVersion.getUrl())) { + return handleNotSnomedSupplement(codeSystem); + } + + return handleSnomedSupplement(codeSystem); + } + + private @NotNull FHIRCodeSystemVersion handleSnomedSupplement(CodeSystem codeSystem) throws ServiceException { + // if no dependency + if (!codeSystem.hasSupplements()) { + throw exception("SNOMED CT CodeSystem supplements must declare which SNOMED CT edition and version they supplement " + + "using the 'supplements' property.", OperationOutcome.IssueType.INVARIANT, 400); + } + // Check dependency is SNOMED + FHIRCodeSystemVersionParams dependencyParams = FHIRHelper.getCodeSystemVersionParams(CanonicalUri.fromString(codeSystem.getSupplements())); + if (!dependencyParams.isSnomed() || dependencyParams.isUnversionedSnomed() + || dependencyParams.isUnspecifiedReleasedSnomed() || dependencyParams.getVersion() == null) { + throw exception(format("The CodeSystem supplement must be a canonical URL with the SNOMED CT code system and a version using the SNOMED CT URI standard to " + + "quote a specific version of a specific edition. For example: http://snomed.info/sct|http://snomed.info/sct/900000000000207008/version/%s0131", + new GregorianCalendar().get(Calendar.YEAR)), OperationOutcome.IssueType.INVARIANT, 400); + } + // Check dependency exists + FHIRCodeSystemVersion dependantVersion = getSnomedVersionOrThrow(dependencyParams); + + // Does the requested code system already exist? + FHIRCodeSystemVersionParams existingCodeSystemParams = FHIRHelper.getCodeSystemVersionParams(codeSystem.getUrl(), codeSystem.getVersion()); + String snomedModule = existingCodeSystemParams.getSnomedModule(); + if (!IdentifierHelper.isConceptId(snomedModule) || snomedModule.length() < 10) {// Long format concept identifier including namespace. + Long suggestedModuleConceptId = null; + if (snomedModule != null && snomedModule.length() == 7) { + // The module id is actually a SNOMED CT namespace... generate concept id that could be used + int namespace = Integer.parseInt(snomedModule); + try { + List<Long> conceptIds = identifierSource.reserveIds(namespace, IdentifierService.EXTENSION_CONCEPT_PARTITION_ID, 1); + if (!conceptIds.isEmpty()) { + suggestedModuleConceptId = conceptIds.get(0); + } + } catch (ServiceException e) { + logger.warn("Failed to generate a concept id using assumed namespace '{}'", namespace, e); + } + } + if (suggestedModuleConceptId != null) { + throw exception(format("The URL of this SNOMED CT CodeSystem supplement must have a version that follows the SNOMED CT URI standard and includes a module id." + + " If a namespace was given in the version URL then the module id '%s' could be used." + + " This id has been generated using the namespace given and is the next id sequence, considering all content currently loaded into Snowstorm.", + suggestedModuleConceptId), + OperationOutcome.IssueType.INVARIANT, 400); + } else { + throw exception("The URL of this SNOMED CT CodeSystem supplement must have a version that follows the SNOMED CT URI standard and includes a module id.", + OperationOutcome.IssueType.INVARIANT, 400); + } + } + org.snomed.snowstorm.core.data.domain.CodeSystem existingCodeSystem = snomedCodeSystemService.findByUriModule(snomedModule); + if (existingCodeSystem != null) { + throw exception("A code system supplement with the same URL and version already exists. Updating SNOMED CT code system supplements is not yet supported.", + OperationOutcome.IssueType.NOTSUPPORTED, 400); + } + + org.snomed.snowstorm.core.data.domain.CodeSystem newCodeSystem = new org.snomed.snowstorm.core.data.domain.CodeSystem(); + org.snomed.snowstorm.core.data.domain.CodeSystem dependentCodeSystem = dependantVersion.getSnomedCodeSystem(); + String shortName = dependentCodeSystem.getShortName() + "-EXP"; + newCodeSystem.setShortName(shortName); + + // Append 2,3,4 etc to the short name to ensure uniqueness + int a = 2; + while (snomedCodeSystemService.find(newCodeSystem.getShortName()) != null) { + newCodeSystem.setShortName(shortName + a); + a++; + } + newCodeSystem.setName("SNOMED CT Postcoordinated Expression Repository"); + newCodeSystem.setBranchPath(String.join("/", dependentCodeSystem.getBranchPath(), newCodeSystem.getShortName())); + newCodeSystem.setUriModuleId(snomedModule); + newCodeSystem.setMaximumPostcoordinationLevel(maxPostcoordinationLevel); + org.snomed.snowstorm.core.data.domain.CodeSystem savedCodeSystem = snomedCodeSystemService.createCodeSystem(newCodeSystem); + return new FHIRCodeSystemVersion(savedCodeSystem); + } + + private @NotNull FHIRCodeSystemVersion handleNotSnomedSupplement(CodeSystem supplement) throws ServiceException { + // if no dependency + if (!supplement.hasSupplements()) { + throw exception("CodeSystem supplements must declare which codesystem they supplement " + + "using the 'supplements' property.", OperationOutcome.IssueType.INVARIANT, 400); + } + // Check dependency is SNOMED + FHIRCodeSystemVersionParams dependencyParams = FHIRHelper.getCodeSystemVersionParams(CanonicalUri.fromString(supplement.getSupplements())); + if (dependencyParams.isSnomed()) { + throw exception("Non SNOMED supplements cannot supplement a SNOMED codesystem on this server", OperationOutcome.IssueType.INVARIANT, 400); + } + // Check dependency exists + FHIRCodeSystemVersion dependentVersion = findCodeSystemVersionOrThrow(dependencyParams); + + CodeSystem updatedCodeSystem = codeSystemRepository.findByUrlAndVersion(dependentVersion.getUrl(), dependentVersion.getVersion()).toHapiCodeSystem(); + + supplement.getExtension().stream().forEach(x -> updatedCodeSystem.addExtension(x)); + + + + //deleteCodeSystemVersion(dependentVersion); + + FHIRCodeSystemVersion updatedDependentVersion = createUpdate(updatedCodeSystem); + + + + return updatedDependentVersion; + } + + public @NotNull CodeSystem addSupplementToCodeSystem(CodeSystem codeSystem, FHIRCodeSystemVersion dependentVersion) { + CodeSystem newCodeSystem = codeSystemRepository.findByUrlAndVersion(dependentVersion.getUrl(), dependentVersion.getVersion()).toHapiCodeSystem(); + Page<FHIRConcept> conceptsPage = conceptService.findConcepts(dependentVersion.getId(), PageRequest.of(0, PAGESIZE)); + List<FHIRConcept> conceptsList = new ArrayList<>(); + for(int x = 0; x < conceptsPage.getTotalPages(); x++){ + conceptsList.addAll(conceptsPage.getContent()); + conceptsPage = conceptService.findConcepts(dependentVersion.getId(), PageRequest.of(x, PAGESIZE)); + } + FHIRGraphBuilder graphBuilder = new FHIRGraphBuilder(); + if (Objects.isNull(dependentVersion.getHierarchyMeaning()) || "is-a".equals(dependentVersion.getHierarchyMeaning())) { + // Record transitive closure of concepts for subsumption testing + for (FHIRConcept concept : conceptsList) { + for (String parentCode : concept.getParents()) { + graphBuilder.addParent(concept.getCode(), parentCode); + } + } + } + + + List<CodeSystem.ConceptDefinitionComponent> concepts = conceptsList.stream().map(concept -> { + CodeSystem.ConceptDefinitionComponent component = new CodeSystem.ConceptDefinitionComponent(); + List<CodeSystem.ConceptDefinitionDesignationComponent> designations = concept.getDesignations().stream().map(fd -> { + CodeSystem.ConceptDefinitionDesignationComponent cd = new CodeSystem.ConceptDefinitionDesignationComponent(); + cd.setLanguage(fd.getLanguage()); + if (StringUtils.isNotEmpty(fd.getUse())) cd.setUse(fd.getUseCoding()); + cd.setValue(fd.getValue()); + fd.getExtensions().forEach( fhirExtension -> { + cd.addExtension(fhirExtension.getHapi()); + }); + return cd; + }).toList(); + concept.getProperties().entrySet().stream().forEach(entry -> { + + entry.getValue().stream().forEach(p -> { + CodeSystem.ConceptPropertyComponent propertyComponent = new CodeSystem.ConceptPropertyComponent(); + propertyComponent.setCode(p.getCode()); + propertyComponent.setValue(p.toHapiValue(dependentVersion.getUrl())); + component.addProperty(propertyComponent); + }); + + }); + concept.getExtensions().entrySet().stream().forEach( entry ->{ + entry.getValue().stream().forEach(e -> { + Extension t = new Extension(); + t.setUrl(e.getCode()); + t.setValue(e.toHapiValue(null)); + component.addExtension(t); + }); + }); + component.setDesignation(designations) + .setCode(concept.getCode()) + .setDisplay(concept.getDisplay()) + .setId(concept.getId()); + return component; + }) + .toList(); + + List<CodeSystem.ConceptDefinitionComponent> finalConcepts = concepts; + concepts.stream().forEach(x ->{ + + Collection<String> children = graphBuilder.getNodeChildren(x.getCode()); + List<CodeSystem.ConceptDefinitionComponent> toAdd = finalConcepts.stream().filter(y -> children.contains(y.getCode())).toList(); + toAdd.stream().forEach(z -> x.addConcept(z)); + }); + + concepts = concepts.stream().filter(x -> graphBuilder.getNodeParents(x.getCode()).isEmpty()).toList(); + + newCodeSystem.setConcept(concepts); + + + List<CodeSystem.ConceptDefinitionComponent> modifiedConceptDefinitions = newCodeSystem.getConcept().stream().map(conceptDefinitionToUpdate -> { + Optional<CodeSystem.ConceptDefinitionComponent> match = codeSystem.getConcept().stream().filter(y -> y.getCode().equals(conceptDefinitionToUpdate.getCode())).findFirst(); + if (match.isPresent()) { + try { + match.get().getExtension().forEach(conceptDefinitionToUpdate::addExtension); + match.get().getProperty().forEach(conceptDefinitionToUpdate::addProperty); + List<CodeSystem.ConceptDefinitionDesignationComponent> newList = new ArrayList<>(); + newList.addAll(conceptDefinitionToUpdate.getDesignation()); + match.get().getDesignation().forEach( des -> newList.add(des)); + conceptDefinitionToUpdate.setDesignation(newList); + } catch (RuntimeException e){ + System.out.println("bla"); + } + + } + return conceptDefinitionToUpdate; + }).toList(); + + newCodeSystem.setConcept(modifiedConceptDefinitions); + return newCodeSystem; + } + private static boolean isSnomedCodeSystemVersionId(String id) { return id.startsWith(SCT_ID_PREFIX); } diff --git a/src/main/java/org/snomed/snowstorm/fhir/services/FHIRConceptService.java b/src/main/java/org/snomed/snowstorm/fhir/services/FHIRConceptService.java index 3d81a99a7..51dc2837e 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/services/FHIRConceptService.java +++ b/src/main/java/org/snomed/snowstorm/fhir/services/FHIRConceptService.java @@ -123,6 +123,13 @@ private void saveAllConceptsOfCodeSystemVersion(FHIRCodeSystemVersion codeSystem } Set<String> props = new HashSet<>(); + //treat extensions as properties, until better solution... + concepts.forEach(concept ->{ + concept.getExtensions().forEach((key,value)->{ + concept.getProperties().put(key,value); + }); + }); + concepts.stream() .filter(concept -> concept.getProperties() != null) .forEach(concept -> props.addAll(concept.getProperties().keySet())); @@ -154,6 +161,10 @@ private void saveAllConceptsOfCodeSystemVersion(FHIRCodeSystemVersion codeSystem } } + public Page<FHIRConcept> findConcepts(String idWithVersion, PageRequest pageRequest){ + return conceptRepository.findByCodeSystemVersion(idWithVersion, pageRequest); + } + public void deleteExistingCodes(String idWithVersion) { Page<FHIRConcept> existingConcepts = conceptRepository.findByCodeSystemVersion(idWithVersion, PageRequest.of(0, 1)); long totalExisting = existingConcepts.getTotalElements(); diff --git a/src/main/java/org/snomed/snowstorm/fhir/services/FHIRHelper.java b/src/main/java/org/snomed/snowstorm/fhir/services/FHIRHelper.java index bcfc1d631..16ee66c42 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/services/FHIRHelper.java +++ b/src/main/java/org/snomed/snowstorm/fhir/services/FHIRHelper.java @@ -106,8 +106,12 @@ public static Integer findParameterIntOrNull(List<Parameters.ParametersParameter @SuppressWarnings("unchecked") public static List<String> findParameterStringListOrNull(List<Parameters.ParametersParameterComponent> parametersParameterComponents, String name) { - return parametersParameterComponents.stream().filter(parametersParameterComponent -> parametersParameterComponent.getName().equals(name)).findFirst() - .map(param -> (List<String>) param.getValue()).orElse(null); + List<String> result = parametersParameterComponents.stream().filter(parametersParameterComponent -> parametersParameterComponent.getName().equals(name)).map(parametersParameterComponent -> parametersParameterComponent.getValue().primitiveValue()).toList(); + if (result.isEmpty()){ + return null; + } else { + return result; + } } public static String getDisplayLanguage(String displayLanguageParam, String acceptHeader) { diff --git a/src/main/java/org/snomed/snowstorm/fhir/services/FHIRLoadPackageService.java b/src/main/java/org/snomed/snowstorm/fhir/services/FHIRLoadPackageService.java index 0bb5329d7..b2f6a5af8 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/services/FHIRLoadPackageService.java +++ b/src/main/java/org/snomed/snowstorm/fhir/services/FHIRLoadPackageService.java @@ -7,6 +7,7 @@ import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.OperationOutcome; @@ -91,15 +92,16 @@ public void uploadPackageResources(File packageFile, Set<String> resourceUrlsToI codeSystemService.deleteCodeSystemVersion(existingCodeSystemVersion); } } - List<CodeSystem.ConceptDefinitionComponent> concepts = codeSystem.getConcept(); - logger.info("Importing CodeSystem {} with {} concepts from package", codeSystem.getUrl(), concepts != null ? concepts.size() : 0); + logger.info("Creating CodeSystem {}", codeSystem.getUrl()); FHIRCodeSystemVersion codeSystemVersion; try { codeSystemVersion = codeSystemService.createUpdate(codeSystem); } catch (ServiceException e) { throw new IOException("Failed to create FHIR CodeSystem.", e); } - if (concepts != null) { + List<CodeSystem.ConceptDefinitionComponent> concepts = getConcepts(codeSystem, codeSystemVersion); + logger.info("Importing CodeSystem {} with {} concepts from package", codeSystem.getUrl(), concepts != null ? concepts.size() : 0); + if (concepts != null ) { fhirConceptService.saveAllConceptsOfCodeSystemVersion(concepts, codeSystemVersion); } } @@ -130,6 +132,22 @@ public void uploadPackageResources(File packageFile, Set<String> resourceUrlsToI logger.info("Completed import of package {}.", submittedFileName); } + private List<CodeSystem.ConceptDefinitionComponent> getConcepts(CodeSystem codeSystem, FHIRCodeSystemVersion version) { + List<CodeSystem.ConceptDefinitionComponent> concepts; + if (StringUtils.isEmpty(codeSystem.getSupplements())){ + concepts = codeSystem.getConcept(); + } else { + CodeSystem newCodeSystem = codeSystemService.addSupplementToCodeSystem(codeSystem, version); + concepts = newCodeSystem.getConcept(); + } + + + + + + return concepts; + } + private static void validateResources(List<FHIRPackageIndexFile> filesToImport, Set<String> resourceUrlsToImport, boolean importAll, Set<String> supportedResourceTypes) { for (FHIRPackageIndexFile fhirPackageIndexFile : filesToImport) { if (importAll && !supportedResourceTypes.contains(fhirPackageIndexFile.getResourceType())) { diff --git a/src/main/java/org/snomed/snowstorm/fhir/services/FHIRValueSetProvider.java b/src/main/java/org/snomed/snowstorm/fhir/services/FHIRValueSetProvider.java index 04b31a288..a6edc90ed 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/services/FHIRValueSetProvider.java +++ b/src/main/java/org/snomed/snowstorm/fhir/services/FHIRValueSetProvider.java @@ -215,7 +215,8 @@ public ValueSet expandInstance( @OperationParam(name="system-version") StringType systemVersion, @OperationParam(name="check-system-version") StringType checkSystemVersion, @OperationParam(name="force-system-version") StringType forceSystemVersion, - @OperationParam(name="version") StringType version)// Invalid parameter + @OperationParam(name="version") StringType version, + @OperationParam(name="version") CodeType property)// Invalid parameter { ValueSetExpansionParameters params; @@ -225,7 +226,7 @@ public ValueSet expandInstance( } else { params = FHIRValueSetProviderHelper.getValueSetExpansionParameters(id, url, valueSetVersion, context, contextDirection, filter, date, offset, count, includeDesignationsType, designations, includeDefinition, activeType, excludeNested, excludeNotForUI, excludePostCoordinated, displayLanguage, - excludeSystem, systemVersion, checkSystemVersion, forceSystemVersion, version); + excludeSystem, systemVersion, checkSystemVersion, forceSystemVersion, version, property); } return valueSetService.expand(params, FHIRHelper.getDisplayLanguage(params.getDisplayLanguage(), request.getHeader(ACCEPT_LANGUAGE_HEADER))); } @@ -255,7 +256,8 @@ public ValueSet expandType( @OperationParam(name="system-version") StringType systemVersion, @OperationParam(name="check-system-version") StringType checkSystemVersion, @OperationParam(name="force-system-version") StringType forceSystemVersion, - @OperationParam(name="version") StringType version)// Invalid parameter + @OperationParam(name="version") StringType version, + @OperationParam(name="property") CodeType property)// Invalid parameter { logger.info(FHIRValueSetProviderHelper.getFullURL(request)); ValueSetExpansionParameters params; @@ -275,10 +277,10 @@ public ValueSet expandType( } else { params = FHIRValueSetProviderHelper.getValueSetExpansionParameters(null, url, valueSetVersion, context, contextDirection, filter, date, offset, count, includeDesignationsType, designations, includeDefinition, activeType, excludeNested, excludeNotForUI, excludePostCoordinated, displayLanguage, - excludeSystem, systemVersion, checkSystemVersion, forceSystemVersion, version); + excludeSystem, systemVersion, checkSystemVersion, forceSystemVersion, version, property); } - return valueSetService.expand(params, FHIRHelper.getDisplayLanguage(params.getDisplayLanguage(), request.getHeader(ACCEPT_LANGUAGE_HEADER))); + return valueSetService.expand(params, request.getHeader(ACCEPT_LANGUAGE_HEADER)); } @Operation(name="$validate-code", idempotent=true) diff --git a/src/main/java/org/snomed/snowstorm/fhir/services/FHIRValueSetProviderHelper.java b/src/main/java/org/snomed/snowstorm/fhir/services/FHIRValueSetProviderHelper.java index 05b61d1ef..de68755c0 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/services/FHIRValueSetProviderHelper.java +++ b/src/main/java/org/snomed/snowstorm/fhir/services/FHIRValueSetProviderHelper.java @@ -60,8 +60,8 @@ static ValueSetExpansionParameters getValueSetExpansionParameters(IdType id, fin findParameterCanonicalOrNull(parametersParameterComponents, "system-version"), findParameterCanonicalOrNull(parametersParameterComponents, "check-system-version"), findParameterCanonicalOrNull(parametersParameterComponents, "force-system-version"), - findParameterStringOrNull(parametersParameterComponents, "version")); - + findParameterStringOrNull(parametersParameterComponents, "version"), + findParameterStringOrNull(parametersParameterComponents, "property")); } static ValueSetExpansionParameters getValueSetExpansionParameters( @@ -86,7 +86,8 @@ static ValueSetExpansionParameters getValueSetExpansionParameters( final StringType systemVersion, final StringType checkSystemVersion, final StringType forceSystemVersion, - final StringType version) { + final StringType version, + final CodeType property) { try { return new ValueSetExpansionParameters( @@ -112,7 +113,8 @@ static ValueSetExpansionParameters getValueSetExpansionParameters( CanonicalUri.fromString(getOrNull(systemVersion)), CanonicalUri.fromString(getOrNull(checkSystemVersion)), CanonicalUri.fromString(getOrNull(forceSystemVersion)), - getOrNull(version)); + getOrNull(version), + getOrNull(property)); } catch (URISyntaxException e) { throw FHIRHelper.exception("Invalid url parameter.", OperationOutcome.IssueType.INVALID, 400); } diff --git a/src/main/java/org/snomed/snowstorm/fhir/services/FHIRValueSetService.java b/src/main/java/org/snomed/snowstorm/fhir/services/FHIRValueSetService.java index b36fdb3bb..f41d8792e 100644 --- a/src/main/java/org/snomed/snowstorm/fhir/services/FHIRValueSetService.java +++ b/src/main/java/org/snomed/snowstorm/fhir/services/FHIRValueSetService.java @@ -8,6 +8,7 @@ import io.kaicode.elasticvc.api.VersionControlHelper; import it.unimi.dsi.fastutil.longs.LongArrayList; +import org.apache.commons.lang3.tuple.Pair; import org.hl7.fhir.r4.model.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -61,6 +62,21 @@ @Service public class FHIRValueSetService { + public static final String[] URLS = {"http://hl7.org/fhir/StructureDefinition/itemWeight", + "http://hl7.org/fhir/StructureDefinition/valueset-label", + "http://hl7.org/fhir/StructureDefinition/valueset-conceptOrder", + "http://hl7.org/fhir/StructureDefinition/valueset-deprecated", + "http://hl7.org/fhir/StructureDefinition/valueset-concept-definition", + "http://hl7.org/fhir/StructureDefinition/valueset-supplement" + }; + + public static final HashMap<String,String> PROPERTY_TO_URL = new HashMap<>(); + + static{ + PROPERTY_TO_URL.put("definition","http://hl7.org/fhir/concept-properties#definition"); + PROPERTY_TO_URL.put("prop","http://hl7.org/fhir/test/CodeSystem/properties#prop"); + } + // Constant to help with "?fhir_vs=refset" public static final String REFSETS_WITH_MEMBERS = "Refsets"; @@ -178,7 +194,6 @@ public ValueSet expand(final ValueSetExpansionParameters params, String displayL notSupported("context", params.getContext()); notSupported("contextDirection", params.getContextDirection()); notSupported("date", params.getDate()); - notSupported("designation", params.getDesignations()); notSupported("excludeNotForUI", params.getExcludeNotForUI()); notSupported("excludePostCoordinated", params.getExcludePostCoordinated()); notSupported("version", params.getVersion());// Not part of the FHIR API spec but requested under MAINT-1363 @@ -234,7 +249,7 @@ public ValueSet expand(final ValueSetExpansionParameters params, String displayL copyright = SNOMED_VALUESET_COPYRIGHT; FHIRCodeSystemVersion codeSystemVersion = allInclusionVersions.iterator().next(); - List<LanguageDialect> languageDialects = ControllerHelper.parseAcceptLanguageHeader(displayLanguage); + List<LanguageDialect> languageDialects = ControllerHelper.parseAcceptLanguageHeader(FHIRHelper.getDisplayLanguage(params.getDisplayLanguage(),displayLanguage)); // Constraints: // - Elasticsearch prevents us from requesting results beyond the first 10K @@ -377,6 +392,11 @@ public ValueSet expand(final ValueSetExpansionParameters params, String displayL Map<String, String> idAndVersionToUrl = allInclusionVersions.stream() .collect(Collectors.toMap(FHIRCodeSystemVersion::getId, FHIRCodeSystemVersion::getUrl)); + Map<String, String> idAndVersionToLanguage = allInclusionVersions.stream() + .collect(Collectors.toMap(FHIRCodeSystemVersion::getId, FHIRCodeSystemVersion::getLanguage)); + allInclusionVersions.forEach(codeSystemVersion -> { + codeSystemVersion.getExtensions().forEach(hapiValueSet::addExtension); + }); ValueSet.ValueSetExpansionComponent expansion = new ValueSet.ValueSetExpansionComponent(); String id = UUID.randomUUID().toString(); expansion.setId(id); @@ -384,6 +404,12 @@ public ValueSet expand(final ValueSetExpansionParameters params, String displayL expansion.setTimestamp(new Date()); Optional.ofNullable(params.getActiveOnly()).ifPresent(x->expansion.addParameter(new ValueSet.ValueSetExpansionParameterComponent(new StringType("activeOnly")).setValue(new BooleanType(x)))); Optional.ofNullable(params.getExcludeNested()).ifPresent(x->expansion.addParameter(new ValueSet.ValueSetExpansionParameterComponent(new StringType("excludeNested")).setValue(new BooleanType(x)))); + Optional.ofNullable(params.getIncludeDesignations()).ifPresent(x->expansion.addParameter(new ValueSet.ValueSetExpansionParameterComponent(new StringType("includeDesignations")).setValue(new BooleanType(x)))); + Optional.ofNullable(params.getDesignations()).ifPresent(x->{ + x.stream().forEach( language -> { + expansion.addParameter(new ValueSet.ValueSetExpansionParameterComponent(new StringType("designation")).setValue(new StringType(language))); + }); + }); allInclusionVersions.forEach(codeSystemVersion -> { if (codeSystemVersion.getVersion() != null) { expansion.addParameter(new ValueSet.ValueSetExpansionParameterComponent(new StringType("version")) @@ -393,25 +419,98 @@ public ValueSet expand(final ValueSetExpansionParameters params, String displayL } } ); - //this line was removed because of the GG tests. - //expansion.addParameter(new ValueSet.ValueSetExpansionParameterComponent(new StringType("displayLanguage")).setValue(new StringType(displayLanguage))); + + hapiValueSet.getExtension().forEach(ext ->{ + if(ext.getUrl().equals("http://hl7.org/fhir/StructureDefinition/valueset-supplement")){ + expansion.addParameter(new ValueSet.ValueSetExpansionParameterComponent(new StringType("used-supplement")) + .setValue(ext.getValue())); + } + + }); + + + Optional.ofNullable(params.getProperty()).ifPresent( x ->{ + addPropertyToExpansion(x, PROPERTY_TO_URL.get(x), expansion); + } + ); + final String fhirDisplayLanguage; + if(Optional.ofNullable(params.getDisplayLanguage()).isPresent()){ + fhirDisplayLanguage = params.getDisplayLanguage(); + expansion.addParameter(new ValueSet.ValueSetExpansionParameterComponent(new StringType("displayLanguage")).setValue(new CodeType(fhirDisplayLanguage))); + } else if (hasDisplayLanguage(hapiValueSet)){ + fhirDisplayLanguage = hapiValueSet.getCompose().getExtensionByUrl("http://hl7.org/fhir/tools/StructureDefinion/valueset-expansion-param").getExtensionString("value"); + expansion.addParameter(new ValueSet.ValueSetExpansionParameterComponent(new StringType("displayLanguage")).setValue(new CodeType(fhirDisplayLanguage))); + + } else if (displayLanguage != null){ + fhirDisplayLanguage = displayLanguage; + expansion.addParameter(new ValueSet.ValueSetExpansionParameterComponent(new StringType("displayLanguage")).setValue(new CodeType(fhirDisplayLanguage))); + } else { + fhirDisplayLanguage = null; + } + + expansion.setContains(conceptsPage.stream().map(concept -> { + List<ValueSet.ConceptReferenceComponent> references = hapiValueSet.getCompose().getInclude().stream() + .flatMap(set -> set.getConcept().stream()).filter(c -> c.getCode().equals(concept.getCode())).toList(); + + ValueSet.ValueSetExpansionContainsComponent component = new ValueSet.ValueSetExpansionContainsComponent() .setSystem(idAndVersionToUrl.get(concept.getCodeSystemVersion())) .setCode(concept.getCode()) .setInactiveElement(concept.isActive() ? null : new BooleanType(true)) .setDisplay(concept.getDisplay()); - concept.getProperties().getOrDefault("status",Collections.emptyList()).stream().filter(x -> x.getValue().equals("retired")).findFirst().ifPresent(x-> component.setAbstract(true)); - concept.getProperties().getOrDefault("status",Collections.emptyList()).stream().filter(x -> x.getValue().equals("retired")).findFirst().ifPresent(x-> component.setInactive(true)); - if (includeDesignations) { - for (FHIRDesignation designation : concept.getDesignations()) { - ValueSet.ConceptReferenceDesignationComponent designationComponent = new ValueSet.ConceptReferenceDesignationComponent(); - designationComponent.setLanguage(designation.getLanguage()); - designationComponent.setUse(designation.getUseCoding()); - designationComponent.setValue(designation.getValue()); - component.addDesignation(designationComponent); + + concept.getProperties().entrySet().forEach( p -> { + if (p.getKey().equals("status")){ + p.getValue().stream() + .filter(x -> x.getValue().equals("retired")) + .findFirst() + .ifPresent(x-> { + component.setAbstract(true); + component.setInactive(true); + }); + + } else if (p.getKey().equals("http://hl7.org/fhir/StructureDefinition/itemWeight")){ + p.getValue().stream() + .findFirst() + .ifPresent(y-> { + addPropertyToContains("weight", component, y.toHapiValue(null)); + addPropertyToExpansion("weight", "http://hl7.org/fhir/concept-properties#itemWeight", expansion); + }); + } else if (p.getKey().equals("http://hl7.org/fhir/StructureDefinition/codesystem-label")){ + p.getValue().stream() + .findFirst() + .ifPresent(y-> { + addPropertyToContains("label", component, y.toHapiValue(null)); + addPropertyToExpansion("label", "http://hl7.org/fhir/concept-properties#label", expansion); + }); + } else if (p.getKey().equals("http://hl7.org/fhir/StructureDefinition/codesystem-conceptOrder")){ + p.getValue().stream() + .findFirst() + .ifPresent(y-> { + addPropertyToContains("order", component, new DecimalType(y.toHapiValue(null).primitiveValue())); + addPropertyToExpansion("order", "http://hl7.org/fhir/concept-properties#order", expansion); + }); } - } + }); + + Optional.ofNullable(params.getProperty()).ifPresent(x ->{ + List<FHIRProperty> properties =concept.getProperties().getOrDefault(x, Collections.emptyList()); + properties.stream() + .findFirst() + .ifPresent(y-> { + addPropertyToContains(y.getCode(), component, y.toHapiValue(null)); + }); + }); + + concept.getExtensions().forEach((key, value) ->{ + value.stream().filter(x ->!x.isSpecialExtension()).forEach( fe ->{ + //addition of these extensions is optional according to the G.G. tests + //component.addExtension(fe.getCode(), fe.toHapiValue(null)); + }); + }); + addInfoFromReferences(component, references); + setDisplayAndDesignations(component, concept, idAndVersionToLanguage.get(concept.getCodeSystemVersion()), includeDesignations, fhirDisplayLanguage, params.getDesignations()); return component; }) .collect(Collectors.toList())); @@ -432,6 +531,214 @@ public ValueSet expand(final ValueSetExpansionParameters params, String displayL } return hapiValueSet; + + + } + + private static boolean hasDisplayLanguage(ValueSet hapiValueSet) { + return Optional.ofNullable(hapiValueSet.getCompose().getExtensionByUrl("http://hl7.org/fhir/tools/StructureDefinion/valueset-expansion-param")).isPresent() && "displayLanguage".equals(hapiValueSet.getCompose().getExtensionByUrl("http://hl7.org/fhir/tools/StructureDefinion/valueset-expansion-param").getExtensionString("name")); + } + + private static void setDisplayAndDesignations(ValueSet.ValueSetExpansionContainsComponent component, FHIRConcept concept, String defaultConceptLanguage, boolean includeDesignations, String displayLanguage, List<String> designationLanguages) { + List<String> designationLang = Optional.ofNullable(designationLanguages).orElse(Collections.emptyList()).stream().map(x -> { + String[] systemAndLanguage = x.split("\\|"); + if (systemAndLanguage.length < 2){ + return systemAndLanguage[0]; + } else { + return systemAndLanguage[1]; + } + + }).toList(); + Map<String, ValueSet.ConceptReferenceDesignationComponent> languageToDesignation = new HashMap<>(); + Map<String, List<Locale>> languageToVarieties = new HashMap<>(); + List<Pair<LanguageDialect, Double>> weightedLanguages = ControllerHelper.parseAcceptLanguageHeaderWithWeights(displayLanguage,true); + Locale defaultLocale = Locale.forLanguageTag(defaultConceptLanguage); + if(languageToVarieties.get(defaultLocale.getLanguage()) == null){ + List<Locale> allVarieties = new ArrayList<>(); + languageToVarieties.put(defaultLocale.getLanguage(),allVarieties); + } + languageToVarieties.get(defaultLocale.getLanguage()).add(defaultLocale); + + languageToDesignation.put(defaultConceptLanguage, new ValueSet.ConceptReferenceDesignationComponent().setValue(component.getDisplay()) + .setLanguage(defaultConceptLanguage) ); + + List<ValueSet.ConceptReferenceDesignationComponent> noLanguage = new ArrayList<>(); + + for (ValueSet.ConceptReferenceDesignationComponent designation : component.getDesignation()){ + if (designation.getLanguage()==null) { + noLanguage.add(designation); + } else { + Locale designationLocale = Locale.forLanguageTag(designation.getLanguage()); + if (languageToVarieties.get(designationLocale.getLanguage()) == null) { + List<Locale> allVarieties = new ArrayList<>(); + languageToVarieties.put(designationLocale.getLanguage(), allVarieties); + } + languageToVarieties.get(designationLocale.getLanguage()).add(designationLocale); + languageToDesignation.put(designation.getLanguage(), designation); + } + + } + + + for (FHIRDesignation designation : concept.getDesignations()) { + ValueSet.ConceptReferenceDesignationComponent designationComponent = new ValueSet.ConceptReferenceDesignationComponent(); + designationComponent.setLanguage(designation.getLanguage()); + designationComponent.setUse(designation.getUseCoding()); + designationComponent.setValue(designation.getValue()); + Optional.ofNullable(designation.getExtensions()).orElse(Collections.emptyList()).forEach( + e -> { + designationComponent.addExtension(e.getHapi()); + } + ); + if (designation.getLanguage()==null) { + noLanguage.add(designationComponent); + } else { + Locale designationLocale = Locale.forLanguageTag(designation.getLanguage()); + if (languageToVarieties.get(designationLocale.getLanguage()) == null) { + List<Locale> allVarieties = new ArrayList<>(); + languageToVarieties.put(designationLocale.getLanguage(), allVarieties); + } + languageToVarieties.get(designationLocale.getLanguage()).add(designationLocale); + languageToDesignation.put(designation.getLanguage(), designationComponent); + } + } + + String requestedLanguage = determineRequestedLanguage(defaultConceptLanguage, weightedLanguages, languageToDesignation.keySet(), languageToVarieties); + if (requestedLanguage == null) { + component.setDisplay(null); + } else { + component.setDisplay(languageToDesignation.get(requestedLanguage).getValue()); + } + + if (includeDesignations) { + List<ValueSet.ConceptReferenceDesignationComponent> newDesignations = new ArrayList<>(); + for (Map.Entry<String, ValueSet.ConceptReferenceDesignationComponent> entry : languageToDesignation.entrySet() ){ + + if (!entry.getKey().equals(requestedLanguage)) { + if (entry.getKey().equals(defaultConceptLanguage)) { + entry.getValue().setUse(new Coding("http://terminology.hl7.org/CodeSystem/designation-usage", "display", null)); + } + + + if(designationLang.isEmpty() || designationLang.contains(entry.getValue().getLanguage())) { + newDesignations.add(entry.getValue()); + } + + } + } + newDesignations.addAll(noLanguage); + component.setDesignation(newDesignations); + } else { + component.setDesignation(Collections.emptyList()); + } + + } + + private static String determineRequestedLanguage(String defaultConceptLanguage, List<Pair<LanguageDialect, Double>> weightedLanguages, Set<String> availableVarieties, Map<String, List<Locale>> languageToVarieties) { + List<Pair<LanguageDialect,Double>> allowedLanguages = new ArrayList<>(weightedLanguages.stream().filter(x -> (x.getRight()>0d)).toList()); + allowedLanguages.sort( (a,b) ->{ return a.getRight().compareTo(b.getRight())*-1;}); + String requestedLanguage = allowedLanguages.isEmpty() ?defaultConceptLanguage:allowedLanguages.get(0).getLeft().getLanguageCode(); + if (!availableVarieties.contains(requestedLanguage)){ + Locale requested = Locale.forLanguageTag(requestedLanguage); + if(languageToVarieties.get(requested.getLanguage())==null){ + List<String> forbiddenLanguages = weightedLanguages.stream().filter(x -> x.getRight().equals(0d)).map(x -> x.getLeft().getLanguageCode()).toList(); + if(forbiddenLanguages.contains(defaultConceptLanguage)||forbiddenLanguages.contains("*")){ + requestedLanguage = null; + } else { + requestedLanguage = defaultConceptLanguage; + } + } else { + requestedLanguage = languageToVarieties.get(requested.getLanguage()).stream().findFirst().get().toLanguageTag(); + } + } + return requestedLanguage; + } + + private static void addPropertyToContains(String code, ValueSet.ValueSetExpansionContainsComponent component, Type value) { + Extension extension = new Extension(); + extension.addExtension("code", new CodeType(code)); + extension.addExtension("value", value); + extension.setUrl("http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.contains.property"); + component.addExtension(extension); + } + + private static void addPropertyToExpansion(String code, String url, ValueSet.ValueSetExpansionComponent expansion) { + if(expansion.getExtensionsByUrl("http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.property") + .stream() + .filter( extension -> extension.hasExtension("code")) + .noneMatch(extension -> extension.getExtensionByUrl("code").getValue().equalsDeep(new CodeType(code)))) { + Extension expExtension = new Extension(); + expExtension.addExtension("code", new CodeType(code)); + expExtension.addExtension("uri", new UriType(url)); + expExtension.setUrl("http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.property"); + expansion.addExtension(expExtension); + } + } + + private static void removeExtension(Element component,String uri, String uri2, Type value){ + List<Extension> extensions = component.getExtensionsByUrl(uri); + for (Extension extension : extensions) { + List<Extension> extensions2 = extension.getExtensionsByUrl(uri2); + for (Extension item : extensions2) { + if (item.getValue().equalsDeep(value)) { + component.getExtension().remove(extension); + return; + } + } + } + } + + private static void addInfoFromReferences(ValueSet.ValueSetExpansionContainsComponent component, List<ValueSet.ConceptReferenceComponent> references) { + references.stream().filter(reference -> reference.getCode().equals(component.getCode())).forEach( reference -> { + reference.getDesignation().forEach( + rd->{ + Optional<ValueSet.ConceptReferenceDesignationComponent> od = component.getDesignation().stream().filter(ode -> ode.getLanguage().equals(rd.getLanguage())).findFirst(); + od.ifPresentOrElse(x ->{ + x.setValue(rd.getValue()); + rd.getExtension().forEach(x::addExtension); + },()-> component.addDesignation(rd)); + } + ); + reference.getExtension().forEach( + re->{ + if (Arrays.asList(FHIRValueSetService.URLS).contains(re.getUrl())){ + + Extension property = new Extension(); + switch (re.getUrl()){ + case "http://hl7.org/fhir/StructureDefinition/itemWeight": + removeExtension(component,"http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.contains.property","code" ,new CodeType("weight")); + property.addExtension("code",new CodeType("weight")); + property.addExtension("value", re.getValue()); + property.setUrl("http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.contains.property"); + break; + case "http://hl7.org/fhir/StructureDefinition/valueset-label": + removeExtension(component,"http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.contains.property","code" ,new CodeType("label")); + property.addExtension("code",new CodeType("label")); + property.addExtension("value", re.getValue()); + property.setUrl("http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.contains.property"); + break; + case "http://hl7.org/fhir/StructureDefinition/valueset-conceptOrder": + removeExtension(component,"http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.contains.property","code" ,new CodeType("order")); + property.addExtension("code",new CodeType("order")); + property.addExtension("value", new DecimalType(re.getValue().primitiveValue())); + property.setUrl("http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.contains.property"); + break; + case "http://hl7.org/fhir/StructureDefinition/valueset-deprecated": + property = re; + break; + case "http://hl7.org/fhir/StructureDefinition/valueset-concept-definition": + property = re; + break; + default: + } + component.addExtension(property); + } + } + ); + + + }); + } private String getUserRef(ValueSet valueSet) { diff --git a/src/main/java/org/snomed/snowstorm/rest/ControllerHelper.java b/src/main/java/org/snomed/snowstorm/rest/ControllerHelper.java index 55f47ae16..636679b57 100644 --- a/src/main/java/org/snomed/snowstorm/rest/ControllerHelper.java +++ b/src/main/java/org/snomed/snowstorm/rest/ControllerHelper.java @@ -2,6 +2,8 @@ import com.google.common.base.Strings; import io.kaicode.rest.util.branchpathrewrite.BranchPathUriUtil; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; import org.snomed.snowstorm.core.data.domain.ConceptMini; import org.snomed.snowstorm.core.data.services.DialectConfigurationService; import org.snomed.snowstorm.core.data.services.NotFoundException; @@ -157,14 +159,15 @@ public static List<LanguageDialect> parseAcceptLanguageHeaderWithDefaultFallback return languageDialects; } - public static List<LanguageDialect> parseAcceptLanguageHeader(String acceptLanguageHeader) { + + public static List<Pair<LanguageDialect, Double>> parseAcceptLanguageHeaderWithWeights(String acceptLanguageHeader, boolean wildcard){ // en-ie-x-21000220103;q=0.8,en-US;q=0.5 - List<LanguageDialect> languageDialects = new ArrayList<>(); + List<Pair<LanguageDialect,Double>> languageDialectsAndWeights = new ArrayList<>(); if (acceptLanguageHeader == null) { acceptLanguageHeader = ""; } - + Double weight; acceptLanguageHeader = acceptLanguageHeader.replaceAll("\\s+", ""); String[] acceptLanguageList = acceptLanguageHeader.toLowerCase().split(","); for (String acceptLanguage : acceptLanguageList) { @@ -178,30 +181,46 @@ public static List<LanguageDialect> parseAcceptLanguageHeader(String acceptLangu String[] valueAndWeight = acceptLanguage.split(";"); // We don't use the weight, just take the value String value = valueAndWeight[0]; + if (valueAndWeight.length < 2){ + weight = 0.1; + } else { + weight = Double.parseDouble(valueAndWeight[1].substring(2)); + } - Matcher matcher = LANGUAGE_PATTERN.matcher(value); - if (matcher.matches()) { - languageCode = matcher.group(1); - } else if ((matcher = LANGUAGE_AND_REFSET_PATTERN.matcher(value)).matches()) { - languageCode = matcher.group(1); - languageReferenceSet = parseLong(matcher.group(2)); - } else if ((matcher = LANGUAGE_AND_DIALECT_PATTERN.matcher(value)).matches() || (matcher = LANGUAGE_AND_DIALECT_AND_CONTEXT_PATTERN.matcher(value)).matches()) { - languageCode = matcher.group(1); - languageReferenceSet = DialectConfigurationService.instance().findRefsetForDialect(value); - } else if ((matcher = LANGUAGE_AND_DIALECT_AND_REFSET_PATTERN.matcher(value)).matches()) { - languageCode = matcher.group(1); - languageReferenceSet = parseLong(matcher.group(3)); + if("*".equals(value) && wildcard){ + languageCode = value; } else { - throw new IllegalArgumentException("Unexpected value within Accept-Language request header '" + value + "'."); + + Matcher matcher = LANGUAGE_PATTERN.matcher(value); + if (matcher.matches()) { + languageCode = matcher.group(1); + } else if ((matcher = LANGUAGE_AND_REFSET_PATTERN.matcher(value)).matches()) { + languageCode = matcher.group(1); + languageReferenceSet = parseLong(matcher.group(2)); + } else if ((matcher = LANGUAGE_AND_DIALECT_PATTERN.matcher(value)).matches() || (matcher = LANGUAGE_AND_DIALECT_AND_CONTEXT_PATTERN.matcher(value)).matches()) { + languageCode = matcher.group(1); + languageReferenceSet = DialectConfigurationService.instance().findRefsetForDialect(value); + } else if ((matcher = LANGUAGE_AND_DIALECT_AND_REFSET_PATTERN.matcher(value)).matches()) { + languageCode = matcher.group(1); + languageReferenceSet = parseLong(matcher.group(3)); + } else { + throw new IllegalArgumentException("Unexpected value within Accept-Language request header '" + value + "'."); + } } - - LanguageDialect languageDialect = new LanguageDialect(languageCode, languageReferenceSet); - if (!languageDialects.contains(languageDialect)) { + + + Pair<LanguageDialect,Double> languageDialect = new ImmutablePair<>(new LanguageDialect(languageCode, languageReferenceSet), weight); + + if (!languageDialectsAndWeights.contains(languageDialect)) { //Would normally use a Set here, but the order may be important - languageDialects.add(languageDialect); + languageDialectsAndWeights.add(languageDialect); } } - return languageDialects; + return languageDialectsAndWeights; + } + + public static List<LanguageDialect> parseAcceptLanguageHeader(String acceptLanguageHeader) { + return parseAcceptLanguageHeaderWithWeights(acceptLanguageHeader,false).stream().map(x -> x.getLeft()).toList(); } static void validatePageSize(long offset, int limit) {