Skip to content

Commit

Permalink
Merge pull request #1339 from b2ihealthcare/issue/SO-6076-language-ta…
Browse files Browse the repository at this point in the history
…g-support

Improve compatibility with BCP-47 language tags
  • Loading branch information
cmark authored Nov 25, 2024
2 parents 820f68d + bf40e84 commit 70ce83a
Show file tree
Hide file tree
Showing 14 changed files with 633 additions and 257 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,20 @@ default String configureConceptExpand(CodeSystemLookupParameters parameters) {
* @param codeSystem
* @param concept
* @param parameters
* @param acceptLanguage
* @return
*/
default List<CodeSystemLookupResultParameters.Designation> expandDesignations(ServiceProvider context, CodeSystem codeSystem, Concept concept, CodeSystemLookupParameters parameters, String acceptLanguage) {
if (parameters.isPropertyRequested("designation")) {
return concept.getDescriptions()
.stream()
.map(description -> new CodeSystemLookupResultParameters.Designation().setValue(description.getTerm()).setLanguage(description.getLanguage()))
.collect(Collectors.toList());
} else {
default List<CodeSystemLookupResultParameters.Designation> expandDesignations(ServiceProvider context, CodeSystem codeSystem, Concept concept, CodeSystemLookupParameters parameters) {
if (!parameters.isPropertyRequested(CodeSystemLookupParameters.PROPERTY_DESIGNATION)) {
return null;
}

return concept.getDescriptions()
.stream()
.map(description -> new CodeSystemLookupResultParameters.Designation()
.setValue(description.getTerm())
// Split private use extension values returned by Snow Owl into sections of at most 8 characters
.setLanguage(FhirRequest.expandLocale(description.getLanguage())))
.collect(Collectors.toList());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ final class FhirCodeSystemLookupRequest extends FhirRequest<CodeSystemLookupResu
protected CodeSystemLookupResultParameters doExecute(ServiceProvider context, CodeSystem codeSystem) {
validateRequestedProperties(codeSystem);

final String acceptLanguage = extractLocales(parameters.getDisplayLanguage());
final String displayLanguage = compactLocale(parameters.getDisplayLanguage());

FhirCodeSystemLookupConverter converter = context.service(RepositoryManager.class).get(codeSystem.getUserString(TerminologyResource.Fields.TOOLING_ID))
.optionalService(FhirCodeSystemLookupConverter.class)
Expand All @@ -75,7 +75,7 @@ protected CodeSystemLookupResultParameters doExecute(ServiceProvider context, Co
.one()
.filterByCodeSystemUri(resourceUri)
.filterById(parameters.extractCode())
.setLocales(acceptLanguage)
.setLocales(displayLanguage)
.setExpand(conceptExpand)
.buildAsync()
.execute(context)
Expand All @@ -87,7 +87,7 @@ protected CodeSystemLookupResultParameters doExecute(ServiceProvider context, Co
result.setName(codeSystem.getName());
result.setDisplay(concept.getTerm());
result.setVersion(codeSystem.getVersion());
result.setDesignation(converter.expandDesignations(context, codeSystem, concept, parameters, acceptLanguage));
result.setDesignation(converter.expandDesignations(context, codeSystem, concept, parameters));
result.setProperty(converter.expandProperties(context, codeSystem, concept, parameters));

return result;
Expand All @@ -97,7 +97,7 @@ private void validateRequestedProperties(CodeSystem codeSystem) {
final Set<String> requestedProperties = Set.copyOf(parameters.getPropertyValues());
// first check if any of the properties are lookup request properties
final Set<String> nonLookupProperties = Sets.difference(requestedProperties, CodeSystemLookupParameters.OFFICIAL_R5_PROPERTY_VALUES);

// second check if the remaining unsupported properties supported by the CodeSystem either via full URL
final Set<String> supportedProperties = codeSystem.getProperty() == null
? Collections.emptySet()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import com.b2international.fhir.r5.operations.CodeSystemValidateCodeParameters;
import com.b2international.fhir.r5.operations.CodeSystemValidateCodeResultParameters;
import com.b2international.snowowl.core.ResourceURI;
import com.b2international.snowowl.core.ServiceProvider;
import com.b2international.snowowl.core.codesystem.CodeSystemRequests;
import com.b2international.snowowl.core.domain.Concept;
Expand Down Expand Up @@ -58,46 +59,50 @@ final class FhirCodeSystemValidateCodeRequest extends FhirRequest<CodeSystemVali

@Override
public CodeSystemValidateCodeResultParameters doExecute(ServiceProvider context, CodeSystem codeSystem) {
Set<Coding> codings = collectCodingsToValidate(parameters);
Map<String, Coding> codingsById = codings.stream().collect(Collectors.toMap(Coding::getCode, c -> c));

// extract locales from the request
Map<String, Concept> conceptsById = CodeSystemRequests.prepareSearchConcepts()
.setLimit(codingsById.keySet().size())
.filterByCodeSystemUri(FhirModelHelpers.resourceUriFrom(codeSystem))
.filterByIds(codingsById.keySet())
.setLocales(extractLocales(parameters.getDisplayLanguage()))
.buildAsync()
.execute(context)
.stream()
.collect(Collectors.toMap(Concept::getId, c -> c));

// check if both Maps have the same keys and report if not
final Set<Coding> codings = collectCodingsToValidate(parameters);
final String displayLanguage = compactLocale(parameters.getDisplayLanguage());
final ResourceURI codeSystemUri = FhirModelHelpers.resourceUriFrom(codeSystem);

final Map<String, Coding> codingsById = codings.stream()
.collect(Collectors.toMap(
c -> c.getCode(),
c -> c));

final Map<String, Concept> conceptsById = CodeSystemRequests.prepareSearchConcepts()
.setLimit(codingsById.keySet().size())
.filterByCodeSystemUri(codeSystemUri)
.filterByIds(codingsById.keySet())
.setLocales(displayLanguage)
.buildAsync()
.execute(context)
.stream()
.collect(Collectors.toMap(
c -> c.getId(),
c -> c));

// Check if both Maps have the same keys and report if not
Set<String> missingConceptIds = Sets.difference(codingsById.keySet(), conceptsById.keySet());

if (!missingConceptIds.isEmpty()) {
return new CodeSystemValidateCodeResultParameters()
.setResult(false)
.setMessage(String.format("Could not find code%s '%s'.", missingConceptIds.size() == 1 ? "" : "s", ImmutableSortedSet.copyOf(missingConceptIds)));
.setResult(false)
.setMessage(String.format("Could not find code%s '%s'.", missingConceptIds.size() == 1 ? "" : "s", ImmutableSortedSet.copyOf(missingConceptIds)));
}

// iterate over requested IDs to detect if one does not have any concept returned
// XXX it would be great to have support for multiple messages/validation results in a single request
for (String id : codingsById.keySet()) {
// check display if provided
Coding providedCoding = codingsById.get(id);
if (providedCoding.getDisplay() != null) {

Concept concept = conceptsById.get(id);

// TODO what about alternative terms?
// TODO: add more validation functionality that is checked for each coding (eg. alternative terms)
for (final String id : codingsById.keySet()) {
final Coding coding = codingsById.get(id);
final String expectedDisplay = coding.getDisplay();

if (expectedDisplay != null) {
final Concept actualConcept = conceptsById.get(id);
final String actualDisplay = actualConcept.getTerm();

if (!providedCoding.getDisplay().equals(concept.getTerm())) {
if (!expectedDisplay.equals(actualDisplay)) {
return new CodeSystemValidateCodeResultParameters()
.setResult(false)
.setMessage(String.format("Incorrect display '%s' for code '%s'.", providedCoding.getDisplay(), providedCoding.getCode()))
.setDisplay(concept.getTerm());
.setResult(false)
.setMessage(String.format("Incorrect display '%s' for code '%s'.", expectedDisplay, coding.getCode()))
.setDisplay(actualDisplay);
}
}
}
Expand All @@ -110,8 +115,8 @@ private Set<Coding> collectCodingsToValidate(CodeSystemValidateCodeParameters pa

if (parameters.getCode() != null) {
Coding coding = new Coding()
.setCode(parameters.getCode().getValue())
.setDisplay(parameters.getDisplay() != null ? parameters.getDisplay().getValue() : null);
.setCode(parameters.getCode().getValue())
.setDisplay(parameters.getDisplay() != null ? parameters.getDisplay().getValue() : null);

codings.add(coding);
}
Expand All @@ -124,7 +129,7 @@ private Set<Coding> collectCodingsToValidate(CodeSystemValidateCodeParameters pa
if (codeableConcept != null && codeableConcept.getCoding() != null) {
codeableConcept.getCoding().forEach(codings::add);
}

return codings;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,26 @@
*/
package com.b2international.snowowl.fhir.core.request.codesystem;

import java.util.IllformedLocaleException;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;

import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.CodeSystem;
import org.hl7.fhir.r5.model.CodeType;

import com.b2international.commons.CompareUtils;
import com.b2international.commons.StringUtils;
import com.b2international.commons.exceptions.NotFoundException;
import com.b2international.commons.http.AcceptLanguageHeader;
import com.b2international.snowowl.core.RepositoryManager;
import com.b2international.snowowl.core.ServiceProvider;
import com.b2international.snowowl.core.events.Request;
import com.b2international.snowowl.core.uri.ResourceURLSchemaSupport;
import com.b2international.snowowl.fhir.core.Summary;
import com.b2international.snowowl.fhir.core.exceptions.BadRequestException;
import com.b2international.snowowl.fhir.core.request.FhirRequests;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;

/**
Expand Down Expand Up @@ -120,12 +125,80 @@ protected String configureSummary() {
return Summary.TRUE;
}

public static final String extractLocales(CodeType displayLanguage) {
String locales = displayLanguage != null ? displayLanguage.getCode() : null;
if (CompareUtils.isEmpty(locales)) {
locales = AcceptLanguageHeader.DEFAULT_ACCEPT_LANGUAGE_HEADER;
/**
* Converts a BCP-47 language tag (where the private use extension portions
* are split into at most 8 characters, separated by dashes) into the "compact"
* non-standard representation used in Snow Owl's internal API.
*
* @param localeAsCode
* @return
*/
public static final String compactLocale(final CodeType localeAsCode) {
final String locale = (localeAsCode != null) ? localeAsCode.getCode() : null;
if (StringUtils.isEmpty(locale)) {
return AcceptLanguageHeader.DEFAULT_ACCEPT_LANGUAGE_HEADER;
}
return locales;

return compactLocale(locale);
}

public static String compactLocale(final String locale) {
// Parse the input in accordance with BCP-47 grammar (it should be valid)
final Locale.Builder builder = new Locale.Builder();
try {
builder.setLanguageTag(locale);
} catch (IllformedLocaleException ex) {
throw new BadRequestException(ex.getMessage());
}

// Remove hyphen separators from the private use extension
final Locale parsedLocale = builder.build();
final String privateUseExtension = parsedLocale.getExtension(Locale.PRIVATE_USE_EXTENSION);
if (StringUtils.isEmpty(privateUseExtension)) {
return locale;
}

/*
* Remove dashes and replace the old private use extension with the compact one
* (using "-x-" as the anchoring prefix -- it should not appear elsewhere in the
* language tag)
*/
final String separatorsRemovedExtension = privateUseExtension.replace("-", "");
return locale.replace("-x-" + privateUseExtension, "-x-" + separatorsRemovedExtension);
}

/**
* Converts a "compact" locale representation into a BCP-47 language tag by
* splitting the private use extension portions into at most 8 characters,
* separating each section with a dash.
*
* @param locale
* @return
*/
public static final String expandLocale(String locale) {
if (StringUtils.isEmpty(locale)) {
return null;
}

/*
* XXX: Assuming locales returned by Snow Owl are in the form of eg. "en-US" or
* "en-x-1234567890123456789" (We can not use Java's built-in parser as at this
* point the extension breaks length limits and the language tag is invalid)
*
* See FhirLocaleTest#expandSplitPrivateUseExtension for an example.
*/
final int privateUseIdx = locale.lastIndexOf("-x-");
if (privateUseIdx < 0 || privateUseIdx + 3 >= locale.length()) {
return locale;
}

final String separatorsRemovedExtension = locale.substring(privateUseIdx + 3);
final String privateUseExtension = Splitter.fixedLength(8) // split private use portion into 8 character segments
.splitToStream(separatorsRemovedExtension)
.collect(Collectors.joining("-")); // combine again with hyphens

// Replace the old private use extension
return locale.replace("-x-" + separatorsRemovedExtension, "-x-" + privateUseExtension);
}

protected abstract R doExecute(ServiceProvider context, CodeSystem codeSystem);
Expand Down
Loading

0 comments on commit 70ce83a

Please sign in to comment.