rewrite the way language works in value sets

This commit is contained in:
Grahame Grieve 2023-08-25 13:09:36 +02:00
parent d468a61664
commit 7eaa723ec9
11 changed files with 465 additions and 95 deletions

View File

@ -568,10 +568,8 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
}
private void setTerminologyOptions(ValidationOptions options, Parameters pIn) {
if (options != null) {
for (String s : options.getLanguages()) {
pIn.addParameter("displayLanguage", s);
}
if (options != null && options.hasLanguages()) {
pIn.addParameter("displayLanguage", options.getLanguages().toString());
}
}

View File

@ -1048,8 +1048,8 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
}
private void setTerminologyOptions(ValidationOptions options, Parameters pIn) {
for (String s : options.getLanguages()) {
pIn.addParameter("displayLanguage", s);
if (options.hasLanguages()) {
pIn.addParameter("displayLanguage", options.getLanguages().toString());
}
if (options.getValueSetMode() != ValueSetMode.ALL_CHECKS) {
pIn.addParameter("valueSetMode", options.getValueSetMode().toString());

View File

@ -1326,8 +1326,8 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
}
private void setTerminologyOptions(ValidationOptions options, Parameters pIn) {
for (String s : options.getLanguages()) {
pIn.addParameter("displayLanguage", s);
if (options.hasLanguages()) {
pIn.addParameter("displayLanguage", options.getLanguages().toString());
}
if (options.getValueSetMode() != ValueSetMode.ALL_CHECKS) {
pIn.addParameter("valueSetMode", options.getValueSetMode().toString());

View File

@ -18,6 +18,8 @@ import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
import org.hl7.fhir.utilities.TextFile;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader;
import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader.LanguagePreference;
import org.hl7.fhir.utilities.i18n.LanguageFileProducer;
import org.hl7.fhir.utilities.i18n.LanguageFileProducer.LanguageProducerLanguageSession;
import org.hl7.fhir.utilities.i18n.LanguageFileProducer.TextUnit;
@ -201,10 +203,49 @@ public class LanguageUtils {
return res;
}
public static boolean langsMatch(String dstLang, String srcLang) {
public static boolean langsMatchExact(AcceptLanguageHeader langs, String srcLang) {
if (langs == null) {
return false;
}
for (LanguagePreference lang : langs.getLangs()) {
if (lang.getValue() > 0) {
if ("*".equals(lang.getLang())) {
return true;
} else {
return langsMatch(lang.getLang(), srcLang);
}
}
}
return false;
}
public static boolean langsMatch(AcceptLanguageHeader langs, String srcLang) {
if (langs == null) {
return false;
}
for (LanguagePreference lang : langs.getLangs()) {
if (lang.getValue() > 0) {
if ("*".equals(lang.getLang())) {
return true;
} else {
boolean ok = langsMatch(lang.getLang(), srcLang);
if (ok) {
return true;
}
}
}
}
return false;
}
public static boolean langsMatchExact(String dstLang, String srcLang) {
return dstLang == null ? false : dstLang.equals(srcLang);
}
public static boolean langsMatch(String dstLang, String srcLang) {
return dstLang == null ? false : dstLang.startsWith(srcLang) || "*".equals(srcLang);
}
public static void fillSupplement(CodeSystem cs, List<TranslationUnit> list) {
cs.setUserData(SUPPLEMENT_NAME, "true");
for (TranslationUnit tu : list) {

View File

@ -87,11 +87,14 @@ import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent;
import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent;
import org.hl7.fhir.r5.model.CodeSystem.PropertyComponent;
import org.hl7.fhir.r5.model.CodeType;
import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.DataType;
import org.hl7.fhir.r5.model.DateTimeType;
import org.hl7.fhir.r5.model.Enumerations.FilterOperator;
import org.hl7.fhir.r5.model.Extension;
import org.hl7.fhir.r5.model.Factory;
import org.hl7.fhir.r5.model.IntegerType;
import org.hl7.fhir.r5.model.PackageInformation;
import org.hl7.fhir.r5.model.Parameters;
import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent;
@ -112,6 +115,7 @@ import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionParameterComponent;
import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionPropertyComponent;
import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
import org.hl7.fhir.r5.terminologies.ValueSetUtilities;
import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpander.Token;
import org.hl7.fhir.r5.terminologies.providers.CodeSystemProvider;
import org.hl7.fhir.r5.terminologies.providers.CodeSystemProviderExtension;
import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext;
@ -121,11 +125,35 @@ import org.hl7.fhir.r5.terminologies.utilities.ValueSetProcessBase;
import org.hl7.fhir.r5.utils.ToolingExtensions;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader;
import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader.LanguagePreference;
import org.hl7.fhir.utilities.i18n.I18nConstants;
public class ValueSetExpander extends ValueSetProcessBase {
public class Token {
private String system;
private String code;
public Token(String system, String code) {
super();
this.system = system;
this.code = code;
}
public String getSystem() {
return system;
}
public String getCode() {
return code;
}
public boolean matches(Coding use) {
return system.equals(use.getSystem()) && code.equals(use.getCode());
}
public boolean matchesLang(String language) {
return system.equals("urn:ietf:bcp:47") && code.equals(language);
}
}
private static final boolean REPORT_VERSION_ANYWAY = true;
private ValueSet focus;
@ -136,6 +164,9 @@ public class ValueSetExpander extends ValueSetProcessBase {
private boolean checkCodesWhenExpanding;
private boolean includeAbstract = true;
private AcceptLanguageHeader langs;
private List<Token> designations = new ArrayList<>();
public ValueSetExpander(IWorkerContext context, TerminologyOperationContext opContext) {
super(context, opContext);
}
@ -203,31 +234,24 @@ public class ValueSetExpander extends ValueSetProcessBase {
"http://hl7.org/fhir/StructureDefinition/rendering-xhtml");
// display and designations
String srcLang = dispLang;
String dstLang = focus.getLanguage();
boolean usedDisplay = false;
ConceptDefinitionDesignationComponent tu;
if (LanguageUtils.langsMatch(dstLang, dispLang)) {
tu = null; // use display
} else {
tu = expParams.hasParameter("displayLanguage") ? getMatchingLang(designations, expParams.getParameterString("displayLanguage")) : null;
}
if (tu != null) {
n.setDisplay(tu.getValue());
} else if (display != null && (srcLang == null || dstLang == null || LanguageUtils.langsMatch(dstLang, srcLang))) {
ConceptDefinitionDesignationComponent pref = null;
if (langs == null) {
n.setDisplay(display);
usedDisplay = true;
} else {
// we don't have a usable display
if (designations == null) {
designations = new ArrayList<>();
}
designations.add(new ConceptDefinitionDesignationComponent().setLanguage(dispLang).setValue(display).setUse(new Coding().setSystem("http://terminology.hl7.org/CodeSystem/designation-usage").setCode("display")));
pref = findMatchingDesignation(designations);
if (pref != null) {
n.setDisplay(pref.getValue());
}
}
if (expParams.getParameterBool("includeDesignations") && designations != null) {
if (!usedDisplay && display != null) {
n.addDesignation().setLanguage(srcLang).setValue(display);
}
if (expParams.getParameterBool("includeDesignations")) {
for (ConceptDefinitionDesignationComponent t : designations) {
if (t != tu && (t.hasLanguage() || t.hasUse()) && t.getValue() != null) {
if (t != pref && (t.hasLanguage() || t.hasUse()) && t.getValue() != null && passesDesignationFilter(t)) {
ConceptReferenceDesignationComponent d = n.addDesignation();
if (t.getLanguage() != null) {
d.setLanguage(t.getLanguage().trim());
@ -288,6 +312,63 @@ public class ValueSetExpander extends ValueSetProcessBase {
return n;
}
private boolean passesDesignationFilter(ConceptDefinitionDesignationComponent d) {
if (designations.isEmpty()) {
return true;
}
for (Token t : designations) {
if (t.matches(d.getUse()) || t.matchesLang(d.getLanguage())) {
return true;
}
for (Coding c : d.getAdditionalUse()) {
if (t.matches(c)) {
return true;
}
}
}
return false;
}
private ConceptDefinitionDesignationComponent findMatchingDesignation(List<ConceptDefinitionDesignationComponent> designations) {
if (langs == null) {
return null;
}
// we have a list of languages in priority order
// we have a list of designations in no order
// language exact match is preferred
// display is always preferred
for (LanguagePreference lang : langs.getLangs()) {
if (lang.getValue() > 0) {
for (ConceptDefinitionDesignationComponent cd : designations) {
if (isDisplay(cd) && LanguageUtils.langsMatchExact(cd.getLanguage(), lang.getLang())) {
return cd;
}
}
for (ConceptDefinitionDesignationComponent cd : designations) {
if (isDisplay(cd) && LanguageUtils.langsMatch(cd.getLanguage(), lang.getLang())) {
return cd;
}
}
for (ConceptDefinitionDesignationComponent cd : designations) {
if (LanguageUtils.langsMatchExact(cd.getLanguage(), lang.getLang())) {
return cd;
}
}
for (ConceptDefinitionDesignationComponent cd : designations) {
if (LanguageUtils.langsMatch(cd.getLanguage(), lang.getLang())) {
return cd;
}
}
}
}
return null;
}
private boolean isDisplay(ConceptDefinitionDesignationComponent cd) {
return cd.getUse().is("http://terminology.hl7.org/CodeSystem/designation-usage", "display");
}
private boolean filterContainsCode(List<ValueSet> filters, String system, String code, ValueSetExpansionComponent exp) {
for (ValueSet vse : filters) {
checkCanonical(exp, vse, focus);
@ -307,13 +388,17 @@ public class ValueSetExpander extends ValueSetProcessBase {
return false;
}
private ConceptDefinitionDesignationComponent getMatchingLang(List<ConceptDefinitionDesignationComponent> list, String lang) {
for (ConceptDefinitionDesignationComponent t : list)
if (t.getLanguage().equals(lang))
private ConceptDefinitionDesignationComponent getMatchingLang(List<ConceptDefinitionDesignationComponent> list, AcceptLanguageHeader langs) {
for (ConceptDefinitionDesignationComponent t : list) {
if (LanguageUtils.langsMatchExact(langs, t.getLanguage())) {
return t;
for (ConceptDefinitionDesignationComponent t : list)
if (t.getLanguage().startsWith(lang))
}
}
for (ConceptDefinitionDesignationComponent t : list) {
if (LanguageUtils.langsMatch(langs, t.getLanguage())) {
return t;
}
}
return null;
}
@ -519,6 +604,42 @@ public class ValueSetExpander extends ValueSetProcessBase {
return doExpand(source, expParams);
}
private void processParameter(String name, DataType value) {
if (Utilities.existsInList(name, "includeDesignations", "excludeNested", "activeOnly", "offset", "count")) {
focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
focus.getExpansion().addParameter().setName(name).setValue(value);
}
if ("displayLanguage".equals(name)) {
this.langs = new AcceptLanguageHeader(value.primitiveValue(), true);
focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
focus.getExpansion().addParameter().setName(name).setValue(new CodeType(value.primitiveValue()));
}
if ("designation".equals(name)) {
String[] v = value.primitiveValue().split("\\|");
if (v.length != 2 || !Utilities.isAbsoluteUrl(v[0]) || Utilities.noString(v[1])) {
throw new NoTerminologyServiceException("Unable to understand designation parameter "+value.primitiveValue());
}
this.designations.add(new Token(v[0], v[1]));
focus.getExpansion().addParameter().setName(name).setValue(new StringType(value.primitiveValue()));
}
if ("offset".equals(name) && value instanceof IntegerType) {
focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
focus.getExpansion().addParameter().setName(name).setValue(value);
dwc.setOffset(((IntegerType) value).getValue());
if (dwc.getOffset() < 0) {
dwc.setOffset(0);
}
}
if ("count".equals(name)) {
focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
focus.getExpansion().addParameter().setName(name).setValue(value);
dwc.setCount(((IntegerType) value).getValue());
if (dwc.getCount() < 0) {
dwc.setCount(0);
}
}
}
public ValueSetExpansionOutcome doExpand(ValueSet source, Parameters expParams) throws FHIRException, ETooCostly, FileNotFoundException, IOException, CodeSystemProviderExtension {
if (expParams == null)
expParams = makeDefaultExpansion();
@ -532,33 +653,20 @@ public class ValueSetExpander extends ValueSetProcessBase {
focus.getExpansion().setTimestampElement(DateTimeType.now());
focus.getExpansion().setIdentifier(Factory.createUUID());
checkCanonical(focus.getExpansion(), focus, focus);
for (Extension ext : focus.getCompose().getExtensionsByUrl("http://hl7.org/fhir/tools/StructureDefinion/valueset-expansion-param")) {
processParameter(ext.getExtensionString("name"), ext.getExtensionByUrl("value").getValue());
}
for (ParametersParameterComponent p : expParams.getParameter()) {
if (Utilities.existsInList(p.getName(), "includeDesignations", "excludeNested", "activeOnly", "offset", "count")) {
focus.getExpansion().addParameter().setName(p.getName()).setValue(p.getValue());
}
if ("displayLanguage".equals(p.getName()) && (!expParams.hasLanguage() || !expParams.getLanguage().equals(p.getValue().primitiveValue()))) {
focus.getExpansion().addParameter().setName(p.getName()).setValue(p.getValue());
}
if ("offset".equals(p.getName()) && p.hasValueIntegerType()) {
dwc.setOffset(p.getValueIntegerType().getValue());
if (dwc.getOffset() < 0) {
dwc.setOffset(0);
}
}
if ("count".equals(p.getName()) && p.hasValueIntegerType()) {
dwc.setCount(p.getValueIntegerType().getValue());
if (dwc.getCount() < 0) {
dwc.setCount(0);
}
}
processParameter(p.getName(), p.getValue());
}
for (Extension s : focus.getExtensionsByUrl(ExtensionConstants.EXT_VSSUPPLEMENT)) {
requiredSupplements.add(s.getValue().primitiveValue());
}
if (expParams.hasLanguage()) {
focus.setLanguage(expParams.getLanguage());
if (langs == null && focus.hasLanguage()) {
langs = new AcceptLanguageHeader(focus.getLanguage(), true);
} else if (langs != null && langs.hasChosen()) {
focus.setLanguage(langs.getChosen());
}
try {
if (source.hasCompose()) {
@ -610,6 +718,7 @@ public class ValueSetExpander extends ValueSetProcessBase {
return new ValueSetExpansionOutcome(focus);
}
private Parameters makeDefaultExpansion() {
Parameters res = new Parameters();
res.addParameter("excludeNested", true);

View File

@ -47,9 +47,11 @@ import org.hl7.fhir.exceptions.NoTerminologyServiceException;
import org.hl7.fhir.r5.context.ContextUtilities;
import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult;
import org.hl7.fhir.r5.elementmodel.LanguageUtils;
import org.hl7.fhir.r5.extensions.ExtensionConstants;
import org.hl7.fhir.r5.model.CanonicalType;
import org.hl7.fhir.r5.model.CodeSystem;
import org.hl7.fhir.r5.model.CodeType;
import org.hl7.fhir.r5.model.Enumerations.CodeSystemContentMode;
import org.hl7.fhir.r5.model.Enumerations.FilterOperator;
import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
@ -90,6 +92,7 @@ import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationConte
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader;
import org.hl7.fhir.utilities.i18n.I18nConstants;
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
import org.hl7.fhir.utilities.validation.ValidationOptions;
@ -191,6 +194,7 @@ public class ValueSetValidator extends ValueSetProcessBase {
public ValidationResult validateCode(String path, CodeableConcept code) throws FHIRException {
opContext.deadCheck();
checkValueSetOptions();
// first, we validate the codings themselves
ValidationProcessInfo info = new ValidationProcessInfo();
@ -370,6 +374,8 @@ public class ValueSetValidator extends ValueSetProcessBase {
public ValidationResult validateCode(String path, Coding code) throws FHIRException {
opContext.deadCheck();
checkValueSetOptions();
String warningMessage = null;
// first, we validate the concept itself
@ -550,6 +556,21 @@ public class ValueSetValidator extends ValueSetProcessBase {
return res;
}
private void checkValueSetOptions() {
if (valueset != null) {
for (Extension ext : valueset.getCompose().getExtensionsByUrl("http://hl7.org/fhir/tools/StructureDefinion/valueset-expansion-param")) {
var name = ext.getExtensionString("name");
var value = ext.getExtensionByUrl("value").getValue();
if ("displayLanguage".equals(name)) {
options.setLanguages(value.primitiveValue());
}
}
if (!options.hasLanguages() && valueset.hasLanguage()) {
options.addLanguage(valueset.getLanguage());
}
}
}
private static final Set<String> SERVER_SIDE_LIST = new HashSet<>(Arrays.asList("http://fdasis.nlm.nih.gov", "http://hl7.org/fhir/sid/ndc", "http://loinc.org", "http://snomed.info/sct", "http://unitsofmeasure.org",
"http://unstats.un.org/unsd/methods/m49/m49.htm", "http://varnomen.hgvs.org", "http://www.nlm.nih.gov/research/umls/rxnorm", "https://www.usps.com/",
"urn:ietf:bcp:13","urn:ietf:bcp:47","urn:ietf:rfc:3986", "urn:iso:std:iso:3166","urn:iso:std:iso:4217", "urn:oid:1.2.36.1.2001.1005.17"));
@ -736,7 +757,7 @@ public class ValueSetValidator extends ValueSetProcessBase {
}
}
if (b.count() == 0) {
String msg = context.formatMessagePlural(options.getLanguages().size(), I18nConstants.NO_VALID_DISPLAY_FOUND, code.getSystem(), code.getCode(), code.getDisplay(), options.langSummary());
String msg = context.formatMessagePlural(options.getLanguages().getLangs().size(), I18nConstants.NO_VALID_DISPLAY_FOUND, code.getSystem(), code.getCode(), code.getDisplay(), options.langSummary());
return new ValidationResult(IssueSeverity.WARNING, msg, code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs), makeIssue(IssueSeverity.WARNING, IssueType.INVALID, path+".display", msg)).setStatus(inactive, status);
} else {
String msg = context.formatMessagePlural(b.count(), ws ? I18nConstants.DISPLAY_NAME_WS_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF : I18nConstants.DISPLAY_NAME_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF, code.getSystem(), code.getCode(), b.toString(), code.getDisplay(), options.langSummary());
@ -756,10 +777,10 @@ public class ValueSetValidator extends ValueSetProcessBase {
if (!options.hasLanguages()) {
return true;
}
if (options.getLanguages().contains(language)) {
if (LanguageUtils.langsMatch(options.getLanguages(), language)) {
return true;
}
if (language == null && (options.getLanguages().contains("en") || options.getLanguages().contains("en-US") || options.isEnglishOk())) {
if (language == null && (options.langSummary().contains("en") || options.langSummary().contains("en-US") || options.isEnglishOk())) {
return true;
}
return false;
@ -1285,20 +1306,20 @@ public class ValueSetValidator extends ValueSetProcessBase {
if (!options.hasLanguages()) {
return cc.getDisplay();
}
if (options.getLanguages().contains(valueset.getLanguage())) {
if (LanguageUtils.langsMatch(options.getLanguages(), valueset.getLanguage())) {
return cc.getDisplay();
}
// if there's no language, we default to accepting the displays as (US) English
if (valueset.getLanguage() == null && (options.getLanguages().contains("en") || options.getLanguages().contains("en-US"))) {
if (valueset.getLanguage() == null && (options.langSummary().contains("en") || options.langSummary().contains("en-US"))) {
return cc.getDisplay();
}
for (ConceptReferenceDesignationComponent d : cc.getDesignation()) {
if (!d.hasUse() && options.getLanguages().contains(d.getLanguage())) {
if (!d.hasUse() && LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
return d.getValue();
}
}
for (ConceptReferenceDesignationComponent d : cc.getDesignation()) {
if (options.getLanguages().contains(d.getLanguage())) {
if (LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
return d.getValue();
}
}
@ -1310,20 +1331,20 @@ public class ValueSetValidator extends ValueSetProcessBase {
if (!options.hasLanguages()) {
return cc.getDisplay();
}
if (cs != null && options.getLanguages().contains(cs.getLanguage())) {
if (cs != null && LanguageUtils.langsMatch(options.getLanguages(), cs.getLanguage())) {
return cc.getDisplay();
}
// if there's no language, we default to accepting the displays as (US) English
if ((cs == null || cs.getLanguage() == null) && (options.getLanguages().contains("en") || options.getLanguages().contains("en-US"))) {
if ((cs == null || cs.getLanguage() == null) && (options.langSummary().contains("en") || options.langSummary().contains("en-US"))) {
return cc.getDisplay();
}
for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) {
if (!d.hasUse() && options.getLanguages().contains(d.getLanguage())) {
if (!d.hasUse() && LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
return d.getValue();
}
}
for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) {
if (options.getLanguages().contains(d.getLanguage())) {
if (LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
return d.getValue();
}
}

View File

@ -0,0 +1,144 @@
package org.hl7.fhir.utilities.i18n;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader.LanguagePreference;
import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader.LanguageSorter;
import net.sf.saxon.functions.Lang;
public class AcceptLanguageHeader {
public class LanguageSorter implements Comparator<LanguagePreference> {
@Override
public int compare(LanguagePreference o1, LanguagePreference o2) {
if (o1.getValue() == o2.getValue()) {
return o1.getOrder() - o2.getOrder();
} else if (o1.getValue() > o2.getValue()) {
return -1;
} else {
return 1;
}
}
}
public class LanguagePreference {
private int order;
private String lang;
private double value;
public String getLang() {
return lang;
}
public double getValue() {
return value;
}
public int getOrder() {
return order;
}
public LanguagePreference(int order, String lang, double value) {
super();
this.order = order;
this.lang = lang;
this.value = value;
}
@Override
public String toString() {
if (value == 0) {
return lang;
} else {
return lang+"; q="+Double.toString(value);
}
}
}
private String source;
private List<LanguagePreference> langs = new ArrayList<>();
private boolean doWildcard;
public String getSource() {
return source;
}
public List<LanguagePreference> getLangs() {
return langs;
}
public AcceptLanguageHeader(String source, boolean doWildcard) {
super();
this.doWildcard = doWildcard;
this.source = source;
process();
}
private void process() {
langs.clear();
String[] parts = source.split("\\,");
boolean wildcard = false;
for (int i = 0; i < parts.length; i++) {
String lang = parts[i].trim();
double weight = 1;
if (lang.contains(";")) {
String w = lang.substring(lang.indexOf(";")+1);
if (w.contains("=")) {
w = w.substring(w.indexOf("=")+1);
}
lang = lang.substring(0, lang.indexOf(";"));
weight = Float.valueOf(w);
}
langs.add(new LanguagePreference(i, lang, weight));
wildcard = wildcard || "*".equals(lang);
}
if (!wildcard && doWildcard) {
langs.add(new LanguagePreference(100, "*", 0.01));
}
Collections.sort(langs, new LanguageSorter());
}
public boolean hasChosen() {
for (LanguagePreference lang : langs) {
if (lang.value == 1) {
return true;
}
}
return false;
}
public String getChosen() {
for (LanguagePreference lang : langs) {
if (lang.value == 1) {
return lang.lang;
}
}
return null;
}
public void add(String language) {
source = toString()+","+language;
process();
}
public AcceptLanguageHeader copy() {
return new AcceptLanguageHeader(toString(), doWildcard);
}
@Override
public String toString() {
CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
for (LanguagePreference lang : langs) {
b.append(lang.toString());
}
return b.toString();
}
}

View File

@ -4,13 +4,16 @@ import java.util.HashSet;
import java.util.Set;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader;
import com.google.gson.JsonElement;
public class ValidationOptions {
public enum ValueSetMode {
ALL_CHECKS, CHECK_MEMERSHIP_ONLY, NO_MEMBERSHIP_CHECK
}
private Set<String> languages = new HashSet<>();
private AcceptLanguageHeader langs = null;
private boolean useServer = true;
private boolean useClient = true;
private boolean guessSystem = false;
@ -21,15 +24,19 @@ public class ValidationOptions {
private boolean useValueSetDisplays;
private boolean englishOk = true;
public ValidationOptions(String... languages) {
public ValidationOptions() {
super();
for(String s : languages) {
this.languages.add(s);
}
public ValidationOptions(String language) {
super();
if (!Utilities.noString(language)) {
langs = new AcceptLanguageHeader(language, false);
}
}
public static ValidationOptions defaults() {
return new ValidationOptions("en", "en-US");
return new ValidationOptions("en, en-US");
}
/**
@ -39,12 +46,12 @@ public class ValidationOptions {
*
* @return
*/
public Set<String> getLanguages() {
return languages;
public AcceptLanguageHeader getLanguages() {
return langs;
}
public boolean hasLanguages() {
return languages.size() > 0;
return langs != null;
}
@ -128,7 +135,7 @@ public class ValidationOptions {
return this;
}
ValidationOptions n = this.copy();
n.languages.add(language);
n.addLanguage(language);
return n;
}
@ -187,7 +194,16 @@ public class ValidationOptions {
}
public ValidationOptions addLanguage(String language) {
this.languages.add(language);
if (this.langs == null) {
langs = new AcceptLanguageHeader(language, false);
} else {
langs.add(language);
}
return this;
}
public ValidationOptions setLanguages(String language) {
langs = new AcceptLanguageHeader(language, false);
return this;
}
@ -247,7 +263,7 @@ public class ValidationOptions {
public ValidationOptions copy() {
ValidationOptions n = new ValidationOptions();
n.languages.addAll(languages);
n.langs = langs == null ? null : langs.copy();
n.useServer = useServer;
n.useClient = useClient;
n.guessSystem = guessSystem;
@ -261,19 +277,18 @@ public class ValidationOptions {
public String toJson() {
return "\"langs\":\""+languages.toString()+"\", \"useServer\":\""+Boolean.toString(useServer)+"\", \"useClient\":\""+Boolean.toString(useClient)+"\", "+
return "\"langs\":\""+( langs == null ? "" : langs.toString())+"\", \"useServer\":\""+Boolean.toString(useServer)+"\", \"useClient\":\""+Boolean.toString(useClient)+"\", "+
"\"guessSystem\":\""+Boolean.toString(guessSystem)+"\", \"valueSetMode\":\""+valueSetMode.toString()+"\", \"displayWarningMode\":\""+Boolean.toString(displayWarningMode)+"\", \"versionFlexible\":\""+Boolean.toString(versionFlexible)+"\"";
}
public String langSummary() {
if (languages.size() == 0) {
if (langs == null) {
return "--";
} else {
return String.join("|", Utilities.sorted(languages));
return langs.toString();
}
}
}

View File

@ -0,0 +1,45 @@
package org.hl7.fhir.utilities.i18n;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class AcceptLanguageTests {
@Test
public void testEasy() {
AcceptLanguageHeader hdr = new AcceptLanguageHeader("en", true);
Assertions.assertEquals(1, hdr.getLangs().size());
Assertions.assertEquals(1, hdr.getLangs().get(0).getValue());
Assertions.assertEquals(0, hdr.getLangs().get(0).getOrder());
Assertions.assertEquals("en", hdr.getLangs().get(0).getLang());
}
@Test
public void testComplex() {
AcceptLanguageHeader hdr = new AcceptLanguageHeader("en; q=0.9, de;q=0.7, *", true);
Assertions.assertEquals(3, hdr.getLangs().size());
Assertions.assertEquals(1, hdr.getLangs().get(0).getValue());
Assertions.assertEquals(2, hdr.getLangs().get(0).getOrder());
Assertions.assertEquals("*", hdr.getLangs().get(0).getLang());
Assertions.assertEquals(0.9, hdr.getLangs().get(1).getValue(), 0.001);
Assertions.assertEquals(0, hdr.getLangs().get(1).getOrder());
Assertions.assertEquals("en", hdr.getLangs().get(1).getLang());
Assertions.assertEquals(0.7, hdr.getLangs().get(2).getValue(), 0.001);
Assertions.assertEquals(1, hdr.getLangs().get(2).getOrder());
Assertions.assertEquals("de", hdr.getLangs().get(2).getLang());
}
@Test
public void testBadSyntax() {
AcceptLanguageHeader hdr = new AcceptLanguageHeader("en; 0.9, ; q=0.7, *:0.1", true);
Assertions.assertEquals(3, hdr.getLangs().size());
}
}

View File

@ -142,19 +142,22 @@ public class TerminologyServiceTests {
engine.getContext().setExpansionProfile((org.hl7.fhir.r5.model.Parameters) loadResource("parameters-default.json"));
}
if (setup.test.asString("operation").equals("expand")) {
expand(engine, req, resp, fp, ext);
expand(engine, req, resp, setup.test.asString("Accept-Language"), fp, ext);
} else if (setup.test.asString("operation").equals("validate-code")) {
validate(engine, setup.test.asString("name"), req, resp, fp, ext);
validate(engine, setup.test.asString("name"), req, resp, setup.test.asString("Accept-Language"), fp, ext);
} else {
Assertions.fail("Unknown Operation "+setup.test.asString("operation"));
}
}
private void expand(ValidationEngine engine, Resource req, String resp, String fp, JsonObject ext) throws IOException {
private void expand(ValidationEngine engine, Resource req, String resp, String lang, String fp, JsonObject ext) throws IOException {
org.hl7.fhir.r5.model.Parameters p = ( org.hl7.fhir.r5.model.Parameters) req;
ValueSet vs = engine.getContext().fetchResource(ValueSet.class, p.getParameterValue("url").primitiveValue());
boolean hierarchical = p.hasParameter("excludeNested") ? p.getParameterBool("excludeNested") == false : true;
Assertions.assertNotNull(vs);
if (lang != null && !p.hasParameter("displayLanguage")) {
p.addParameter("displayLanguage", new CodeType(lang));
}
ValueSetExpansionOutcome vse = engine.getContext().expandVS(vs, false, hierarchical, false, p);
if (resp.contains("\"ValueSet\"")) {
if (vse.getValueset() == null) {
@ -230,7 +233,7 @@ public class TerminologyServiceTests {
}
}
private void validate(ValidationEngine engine, String name, Resource req, String resp, String fp, JsonObject ext) throws JsonSyntaxException, FileNotFoundException, IOException {
private void validate(ValidationEngine engine, String name, Resource req, String resp, String lang, String fp, JsonObject ext) throws JsonSyntaxException, FileNotFoundException, IOException {
org.hl7.fhir.r5.model.Parameters p = (org.hl7.fhir.r5.model.Parameters) req;
ValueSet vs = null;
if (p.hasParameter("valueSetVersion")) {
@ -241,6 +244,8 @@ public class TerminologyServiceTests {
ValidationOptions options = new ValidationOptions();
if (p.hasParameter("displayLanguage")) {
options = options.withLanguage(p.getParameterString("displayLanguage"));
} else if (lang != null ) {
options = options.withLanguage(lang);
}
if (p.hasParameter("valueSetMode") && "CHECK_MEMBERSHIP_ONLY".equals(p.getParameterString("valueSetMode"))) {
options = options.withCheckValueSetOnly();

View File

@ -214,15 +214,7 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe
if (!VersionUtilities.isR5Plus(val.getContext().getVersion())) {
val.getBaseOptions().setUseValueSetDisplays(true);
}
val.getBaseOptions().getLanguages().clear();
if (content.has("languages")) {
for (String s : content.get("languages").getAsString().split("\\,")) {
String l = s.trim();
val.getBaseOptions().getLanguages().add(l);
}
} else {
}
val.getBaseOptions().setLanguages(content.get("languages").getAsString());
if (content.has("fetcher") && "standalone".equals(JsonUtilities.str(content, "fetcher"))) {
val.setFetcher(vCurr);