
001package org.hl7.fhir.r5.elementmodel; 002 003import java.io.IOException; 004import java.util.ArrayList; 005import java.util.Arrays; 006import java.util.HashMap; 007import java.util.HashSet; 008import java.util.List; 009import java.util.Map; 010import java.util.Set; 011 012import lombok.extern.slf4j.Slf4j; 013import org.hl7.fhir.r5.context.ContextUtilities; 014import org.hl7.fhir.r5.context.IWorkerContext; 015import org.hl7.fhir.r5.elementmodel.Element.SpecialElement; 016import org.hl7.fhir.r5.extensions.ExtensionDefinitions; 017import org.hl7.fhir.r5.extensions.ExtensionUtilities; 018import org.hl7.fhir.r5.model.Base; 019import org.hl7.fhir.r5.model.CodeSystem; 020import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; 021import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent; 022import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent; 023import org.hl7.fhir.r5.model.ContactDetail; 024import org.hl7.fhir.r5.model.DataType; 025import org.hl7.fhir.r5.model.ElementDefinition; 026import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingAdditionalComponent; 027import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionConstraintComponent; 028import org.hl7.fhir.r5.model.Extension; 029import org.hl7.fhir.r5.model.MarkdownType; 030import org.hl7.fhir.r5.model.PrimitiveType; 031import org.hl7.fhir.r5.model.Property; 032import org.hl7.fhir.r5.model.Resource; 033import org.hl7.fhir.r5.model.StringType; 034import org.hl7.fhir.r5.model.StructureDefinition; 035import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; 036import org.hl7.fhir.r5.utils.UserDataNames; 037import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage; 038import org.hl7.fhir.utilities.Utilities; 039import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader; 040import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader.LanguagePreference; 041import org.hl7.fhir.utilities.i18n.LanguageFileProducer.LanguageProducerLanguageSession; 042import org.hl7.fhir.utilities.i18n.LanguageFileProducer.TextUnit; 043import org.hl7.fhir.utilities.i18n.LanguageFileProducer.TranslationUnit; 044import org.hl7.fhir.utilities.validation.ValidationMessage; 045import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 046import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 047import org.hl7.fhir.utilities.validation.ValidationMessage.Source; 048import org.hl7.fhir.utilities.xhtml.XhtmlNode; 049 050/** 051 * in here: 052 * * generateTranslations 053 * * importFromTranslations 054 * * stripTranslations 055 * * switchLanguage 056 * 057 * in the validator 058 * 059 * @author grahamegrieve 060 * generateTranslations = -langTransform export -src {src} -tgt {tgt} -dest {dest} 061 * importFromTranslations = -langTransform import -src {src} -tgt {tgt} -dest {dest} 062 */ 063@MarkedToMoveToAdjunctPackage 064@Slf4j 065public class LanguageUtils { 066 067 public static final List<String> TRANSLATION_SUPPLEMENT_RESOURCE_TYPES = Arrays.asList("CodeSystem", "StructureDefinition", "Questionnaire"); 068 069 public static class TranslationUnitCollection { 070 List<TranslationUnit> list= new ArrayList<>(); 071 Map<String, TranslationUnit> map = new HashMap<>(); 072 public void add(TranslationUnit tu) { 073 String key = tu.getId()+"||"+tu.getSrcText(); 074 if (!map.containsKey(key)) { 075 map.put(key, tu); 076 list.add(tu); 077 } 078 079 } 080 } 081 IWorkerContext context; 082 private List<String> crlist; 083 084 085 public LanguageUtils(IWorkerContext context) { 086 super(); 087 this.context = context; 088 } 089 090 public void generateTranslations(Element resource, LanguageProducerLanguageSession session) { 091 translate(null, resource, session, resource.fhirType()); 092 } 093 094 095 private void translate(Element parent, Element element, LanguageProducerLanguageSession langSession, String path) { 096 String npath = pathForElement(path, element); 097 if (element.isPrimitive() && isTranslatable(element)) { 098 String base = element.primitiveValue(); 099 if (base != null) { 100 String translation = getSpecialTranslation(path, parent, element, langSession.getTargetLang()); 101 if (translation == null) { 102 translation = element.getTranslation(langSession.getTargetLang()); 103 } 104 langSession.entry(new TextUnit(npath, contextForElement(element), base, translation)); 105 } 106 } 107 for (Element c: element.getChildren()) { 108 if (!c.getName().equals("designation")) { 109 translate(element, c, langSession, npath); 110 } 111 } 112 } 113 114 private String contextForElement(Element element) { 115 throw new Error("Not done yet"); 116 } 117 118 private String getSpecialTranslation(String path, Element parent, Element element, String targetLang) { 119 if (parent == null) { 120 return null; 121 } 122 String npath = parent.getBasePath(); 123 if (Utilities.existsInList(npath, "CodeSystem.concept", "CodeSystem.concept.concept") && "CodeSystem.concept.display".equals(element.getBasePath())) { 124 return getDesignationTranslation(parent, targetLang); 125 } 126 if (Utilities.existsInList(npath, "ValueSet.compose.include.concept") && "ValueSet.compose.include.concept.display".equals(element.getBasePath())) { 127 return getDesignationTranslation(parent, targetLang); 128 } 129 if (Utilities.existsInList(npath, "ValueSet.expansion.contains", "ValueSet.expansion.contains.contains") && "ValueSet.expansion.contains.display".equals(element.getBasePath())) { 130 return getDesignationTranslation(parent, targetLang); 131 } 132 return null; 133 } 134 135 private String getDesignationTranslation(Element parent, String targetLang) { 136 for (Element e : parent.getChildren("designation")) { 137 String lang = e.getNamedChildValue("language"); 138 if (langsMatch(targetLang, lang)) { 139 return e.getNamedChildValue("value"); 140 } 141 } 142 return null; 143 } 144 145 private boolean isTranslatable(Element element) { 146 return element.getProperty().isTranslatable(); 147 } 148 149 private String pathForElement(String path, Element element) { 150 if (element.getSpecial() != null) { 151 String bp = element.getBasePath(); 152 return pathForElement(bp, element.getProperty().getStructure().getType()); 153 } else { 154 return (path == null ? element.getName() : path+"."+element.getName()); 155 } 156 } 157 158 private String pathForElement(String path, String type) { 159 // special case support for metadata elements prior to R5: 160 if (crlist == null) { 161 crlist = new ContextUtilities(context).getCanonicalResourceNames(); 162 } 163 if (crlist.contains(type)) { 164 String fp = path.replace(type+".", "CanonicalResource."); 165 if (Utilities.existsInList(fp, 166 "CanonicalResource.url", "CanonicalResource.identifier", "CanonicalResource.version", "CanonicalResource.name", 167 "CanonicalResource.title", "CanonicalResource.status", "CanonicalResource.experimental", "CanonicalResource.date", 168 "CanonicalResource.publisher", "CanonicalResource.contact", "CanonicalResource.description", "CanonicalResource.useContext", 169 "CanonicalResource.jurisdiction")) { 170 return fp; 171 } 172 } 173 return path; 174 } 175 176 177 public int importFromTranslations(Element resource, List<TranslationUnit> translations) { 178 return importFromTranslations(resource.fhirType(), null, resource, translations, new HashSet<>()); 179 } 180 181 public int importFromTranslations(Element resource, List<TranslationUnit> translations, List<ValidationMessage> messages) { 182 Set<TranslationUnit> usedUnits = new HashSet<>(); 183 int r = 0; 184 if (resource.fhirType().equals("StructureDefinition")) { 185 r = importFromTranslationsForSD(null, resource, translations, usedUnits); 186 } else { 187 r = importFromTranslations(null, null, resource, translations, usedUnits); 188 } 189 for (TranslationUnit t : translations) { 190 if (!usedUnits.contains(t)) { 191 if (messages != null) { 192 messages.add(new ValidationMessage(Source.Publisher, IssueType.INFORMATIONAL, t.getId(), "Unused '"+t.getLanguage()+"' translation '"+t.getSrcText()+"' -> '"+t.getTgtText()+"'", IssueSeverity.INFORMATION)); 193 } 194 } 195 } 196 return r; 197 } 198 199 public int importFromTranslations(Resource resource, List<TranslationUnit> translations, List<ValidationMessage> messages) { 200 Set<TranslationUnit> usedUnits = new HashSet<>(); 201 int r = 0; 202 if (resource.fhirType().equals("StructureDefinition")) { 203 // todo... r = importFromTranslationsForSD(null, resource, translations, usedUnits); 204 } else { 205 r = importResourceFromTranslations(null, resource, translations, usedUnits, resource.fhirType()); 206 } 207 for (TranslationUnit t : translations) { 208 if (!usedUnits.contains(t)) { 209 if (messages != null) { 210 messages.add(new ValidationMessage(Source.Publisher, IssueType.INFORMATIONAL, t.getId(), "Unused '"+t.getLanguage()+"' translation '"+t.getSrcText()+"' -> '"+t.getTgtText()+"'", IssueSeverity.INFORMATION)); 211 } 212 } 213 } 214 return r; 215 } 216 217 218 /* 219 * */ 220 private int importFromTranslationsForSD(Object object, Element resource, List<TranslationUnit> translations, Set<TranslationUnit> usedUnits) { 221 int r = 0; 222 r = r + checkForTranslations(translations, usedUnits, resource, "StructureDefinition.name", "name"); 223 r = r + checkForTranslations(translations, usedUnits, resource, "StructureDefinition.title", "title"); 224 r = r + checkForTranslations(translations, usedUnits, resource, "StructureDefinition.publisher", "publisher"); 225 for (Element cd : resource.getChildrenByName("contact")) { 226 r = r + checkForTranslations(translations, usedUnits, cd, "StructureDefinition.contact.name", "name"); 227 } 228 r = r + checkForTranslations(translations, usedUnits, resource, "StructureDefinition.purpose", "purpose"); 229 r = r + checkForTranslations(translations, usedUnits, resource, "StructureDefinition.copyright", "copyright"); 230 Element diff = resource.getNamedChild("differential"); 231 if (diff != null) { 232 for (Element ed : diff.getChildrenByName("element")) { 233 String id = ed.getNamedChildValue("id"); 234 r = r + checkForTranslations(translations, usedUnits, ed, "StructureDefinition.element."+id+"/label", "label"); 235 r = r + checkForTranslations(translations, usedUnits, ed, "StructureDefinition.element."+id+"/short", "short"); 236 r = r + checkForTranslations(translations, usedUnits, ed, "StructureDefinition.element."+id+"/definition", "definition"); 237 r = r + checkForTranslations(translations, usedUnits, ed, "StructureDefinition.element."+id+"/comment", "comment"); 238 r = r + checkForTranslations(translations, usedUnits, ed, "StructureDefinition.element."+id+"/requirements", "requirements"); 239 r = r + checkForTranslations(translations, usedUnits, ed, "StructureDefinition.element."+id+"/meaningWhenMissing", "meaningWhenMissing"); 240 r = r + checkForTranslations(translations, usedUnits, ed, "StructureDefinition.element."+id+"/orderMeaning", "orderMeaning"); 241 // for (ElementDefinitionConstraintComponent con : ed.getConstraint()) { 242 // addToList(list, lang, con, ed.getId()+"/constraint", "human", con.getHumanElement()); 243 // } 244 // if (ed.hasBinding()) { 245 // addToList(list, lang, ed.getBinding(), ed.getId()+"/b/desc", "description", ed.getBinding().getDescriptionElement()); 246 // for (ElementDefinitionBindingAdditionalComponent ab : ed.getBinding().getAdditional()) { 247 // addToList(list, lang, ab, ed.getId()+"/ab/doco", "documentation", ab.getDocumentationElement()); 248 // addToList(list, lang, ab, ed.getId()+"/ab/short", "shortDoco", ab.getShortDocoElement()); 249 // } 250 // } 251 } 252 } 253 return r; 254 } 255 256 private int checkForTranslations(List<TranslationUnit> translations, Set<TranslationUnit> usedUnits, Element context, String tname, String pname) { 257 int r = 0; 258 Element child = context.getNamedChild(pname); 259 if (child != null) { 260 String v = child.primitiveValue(); 261 if (v != null) { 262 for (TranslationUnit tu : translations) { 263 if (tname.equals(tu.getId()) && v.equals(tu.getSrcText())) { 264 usedUnits.add(tu); 265 child.setTranslation(tu.getLanguage(), tu.getTgtText()); 266 r++; 267 } 268 } 269 } 270 } 271 return r; 272 } 273 274 private int importResourceFromTranslations(Base parent, Base element, List<TranslationUnit> translations, Set<TranslationUnit> usedUnits, String path) { 275 int t = 0; 276 if (element.isPrimitive() && isTranslatable(element, path) && element instanceof org.hl7.fhir.r5.model.Element) { 277 org.hl7.fhir.r5.model.Element e = (org.hl7.fhir.r5.model.Element) element; 278 String base = element.primitiveValue(); 279 if (base != null) { 280 String epath = pathForElement(path, element.fhirType()); 281 Set<TranslationUnit> tlist = findTranslations(epath, base, translations); 282 for (TranslationUnit translation : tlist) { 283 t++; 284 if (!handleAsSpecial(parent, element, translation)) { 285 usedUnits.add(translation); 286 if (translation.getTgtText() != null) { 287 ExtensionUtilities.setLanguageTranslation(e, translation.getLanguage(), translation.getTgtText()); 288 } else { 289 log.warn("?"); 290 } 291 } 292 } 293 } 294 } 295 for (Property c : element.children()) { 296 for (Base v : c.getValues()) { 297 if (!c.getName().equals("designation") && !isTranslation(v)) { 298 t = t + importResourceFromTranslations(element, v, translations, usedUnits, genPath(c, v, path, c.getName())); 299 } 300 } 301 } 302 return t; 303 } 304 305 private String genPath(Property c, Base v, String path, String name) { 306 // special cases: recursion 307 if ("ImplementationGuide.definition.page".equals(path) && "page".equals(name)) { 308 return path; 309 } 310 if ("ValueSet.expansion.contains".equals(path) && "contains".equals(name)) { 311 return path; 312 } 313 if ("ValueSet.expansion.contains".equals(path) && "contains".equals(name)) { 314 return path; 315 } 316 if (v.isResource() && !"contained".equals(name)) { 317 return v.fhirType(); 318 } else { 319 return path+"."+name; 320 } 321 } 322 323 private boolean isTranslation(Base element) { 324 return "Extension".equals(element.fhirType()) && element.getChildByName("url").hasValues() && ExtensionDefinitions.EXT_TRANSLATION.equals(element.getChildByName("url").getValues().get(0).primitiveValue()); 325 } 326 327 private boolean handleAsSpecial(Base parent, Base element, TranslationUnit translation) { 328 return false; 329 } 330 331 private boolean isTranslatable(Base element, String path) { 332 return (Utilities.existsInList(element.fhirType(), "string", "markdown") 333 || isTranslatable(path)) && !isExemptFromTranslations(path); 334 } 335 336 private int importFromTranslations(String path, Element parent, Element element, List<TranslationUnit> translations, Set<TranslationUnit> usedUnits) { 337 String npath = pathForElement(path, element); 338 int t = 0; 339 if (element.isPrimitive() && isTranslatable(element) && !isExemptFromTranslations(npath)) { 340 String base = element.primitiveValue(); 341 if (base != null) { 342 Set<TranslationUnit> tlist = findTranslations(npath, base, translations); 343 for (TranslationUnit translation : tlist) { 344 t++; 345 if (!handleAsSpecial(parent, element, translation)) { 346 element.setTranslation(translation.getLanguage(), translation.getTgtText()); 347 usedUnits.add(translation); 348 } 349 } 350 } 351 } 352 // Create a copy of the children collection before iterating 353 List<Element> childrenCopy = List.copyOf(element.getChildren()); 354 for (Element c : childrenCopy) { 355 if (!c.getName().equals("designation")) { 356 t = t + importFromTranslations(npath, element, c, translations, usedUnits); 357 } 358 } 359 return t; 360 } 361 362 private boolean handleAsSpecial(Element parent, Element element, TranslationUnit translation) { 363 if (parent == null) { 364 return false; 365 } 366 if (Utilities.existsInList(parent.getBasePath(), "CodeSystem.concept", "CodeSystem.concept.concept") && "CodeSystem.concept.display".equals(element.getBasePath())) { 367 return setDesignationTranslation(parent, translation.getLanguage(), translation.getTgtText()); 368 } 369 if (Utilities.existsInList(parent.getBasePath(), "ValueSet.compose.include.concept") && "ValueSet.compose.include.concept.display".equals(element.getBasePath())) { 370 return setDesignationTranslation(parent, translation.getLanguage(), translation.getTgtText()); 371 } 372 if (Utilities.existsInList(parent.getBasePath(), "ValueSet.expansion.contains", "ValueSet.expansion.contains.contains") && "ValueSet.expansion.contains.display".equals(element.getBasePath())) { 373 return setDesignationTranslation(parent, translation.getLanguage(), translation.getTgtText()); 374 } 375 return false; 376 } 377 378 private boolean setDesignationTranslation(Element parent, String targetLang, String translation) { 379 for (Element e : parent.getChildren("designation")) { 380 String lang = e.getNamedChildValue("language"); 381 if (langsMatch(targetLang, lang)) { 382 Element value = e.getNamedChild("value"); 383 if (value != null) { 384 value.setValue(translation); 385 } else { 386 e.addElement("value").setValue(translation); 387 } 388 return true; 389 } 390 } 391 Element d = parent.addElement("designation"); 392 d.addElement("language").setValue(targetLang); 393 d.addElement("value").setValue(translation); 394 return true; 395 } 396 397 private Set<TranslationUnit> findTranslations(String path, String src, List<TranslationUnit> translations) { 398 Set<TranslationUnit> res = new HashSet<>(); 399 for (TranslationUnit translation : translations) { 400 if (path.equals(translation.getId()) && src.equals(translation.getSrcText())) { 401 res.add(translation); 402 } 403 } 404 return res; 405 } 406 407 public static boolean langsMatchExact(AcceptLanguageHeader langs, String srcLang) { 408 if (langs == null) { 409 return false; 410 } 411 for (LanguagePreference lang : langs.getLangs()) { 412 if (lang.getValue() > 0) { 413 if ("*".equals(lang.getLang())) { 414 return true; 415 } else { 416 return langsMatch(lang.getLang(), srcLang); 417 } 418 } 419 } 420 return false; 421 } 422 423 public static boolean langsMatch(AcceptLanguageHeader langs, String srcLang) { 424 if (langs == null) { 425 return false; 426 } 427 for (LanguagePreference lang : langs.getLangs()) { 428 if (lang.getValue() > 0) { 429 if ("*".equals(lang.getLang())) { 430 return true; 431 } else { 432 boolean ok = langsMatch(lang.getLang(), srcLang); 433 if (ok) { 434 return true; 435 } 436 } 437 } 438 } 439 return false; 440 } 441 442 public static boolean langsMatchExact(String dstLang, String srcLang) { 443 return dstLang == null ? false : dstLang.equals(srcLang); 444 } 445 446 public static boolean langsMatch(String dstLang, String srcLang) { 447 if (dstLang == null && srcLang == null) { 448 return true; 449 } if (dstLang == null) { 450 return srcLang.equals("en") || srcLang.startsWith("en-"); 451 } else if (srcLang == null) { 452 return dstLang.equals("en") || dstLang.startsWith("en-"); 453 } else { 454 return dstLang.startsWith(srcLang) || "*".equals(srcLang); 455 } 456 } 457 458 public void fillSupplement(CodeSystem csSrc, CodeSystem csDst, List<TranslationUnit> list) { 459 csDst.setUserData(UserDataNames.LANGUTILS_SOURCE_SUPPLEMENT, csSrc); 460 csDst.setUserData(UserDataNames.LANGUTILS_SOURCE_TRANSLATIONS, list); 461 for (TranslationUnit tu : list) { 462 String code = tu.getId(); 463 String subCode = null; 464 if (code.contains("@")) { 465 subCode = code.substring(code.indexOf("@")+1); 466 code = code.substring(0, code.indexOf("@")); 467 } 468 ConceptDefinitionComponent cdSrc = CodeSystemUtilities.getCode(csSrc, tu.getId()); 469 if (cdSrc == null) { 470 addOrphanTranslation(csSrc, tu); 471 } else { 472 ConceptDefinitionComponent cdDst = CodeSystemUtilities.getCode(csDst, cdSrc.getCode()); 473 if (cdDst == null) { 474 cdDst = csDst.addConcept().setCode(cdSrc.getCode()); 475 } 476 String tt = tu.getTgtText(); 477 if (tt.startsWith("!!")) { 478 tt = tt.substring(3); 479 } 480 if (subCode == null) { 481 cdDst.setDisplay(tt); 482 } else if ("definition".equals(subCode)) { 483 cdDst.setDefinition(tt); 484 } else { 485 boolean found = false; 486 for (ConceptDefinitionDesignationComponent d : cdSrc.getDesignation()) { 487 if (d.hasUse() && subCode.equals(d.getUse().getCode())) { 488 found = true; 489 cdDst.addDesignation().setUse(d.getUse()).setLanguage(tu.getLanguage()).setValue(tt); //.setUserData(SUPPLEMENT_SOURCE, tu); 490 break; 491 } 492 } 493 if (!found) { 494 for (Extension e : cdSrc.getExtension()) { 495 if (subCode.equals(tail(e.getUrl()))) { 496 found = true; 497 cdDst.addExtension().setUrl(e.getUrl()).setValue( 498 e.getValue().fhirType().equals("markdown") ? new MarkdownType(tt) : new StringType(tt)); //.setUserData(SUPPLEMENT_SOURCE, tu); 499 break; 500 } 501 } 502 } 503 if (!found) { 504 addOrphanTranslation(csSrc, tu); 505 } 506 } 507 } 508 } 509 } 510 511 private String tail(String url) { 512 return url.contains("/") ? url.substring(url.lastIndexOf("/")+1) : url; 513 } 514 515 private void addOrphanTranslation(CodeSystem cs, TranslationUnit tu) { 516 List<TranslationUnit> list = (List<TranslationUnit>) cs.getUserData(UserDataNames.LANGUTILS_ORPHAN); 517 if (list == null) { 518 list = new ArrayList<>(); 519 cs.setUserData(UserDataNames.LANGUTILS_ORPHAN, list); 520 } 521 list.add(tu); 522 } 523 524 public String nameForLang(String lang) { 525 // todo: replace with structures from loading languages properly 526 switch (lang) { 527 case "en" : return "English"; 528 case "de" : return "German"; 529 case "es" : return "Spanish"; 530 case "nl" : return "Dutch"; 531 } 532 return Utilities.capitalize(lang); 533 } 534 535 public String titleForLang(String lang) { 536 // todo: replace with structures from loading languages properly 537 switch (lang) { 538 case "en" : return "English"; 539 case "de" : return "German"; 540 case "es" : return "Spanish"; 541 case "nl" : return "Dutch"; 542 } 543 return Utilities.capitalize(lang); 544 } 545 546 public boolean handlesAsResource(Resource resource) { 547 return (resource instanceof CodeSystem && resource.hasUserData(UserDataNames.LANGUTILS_SOURCE_SUPPLEMENT)) || (resource instanceof StructureDefinition); 548 } 549 550 public boolean handlesAsElement(Element element) { 551 return true; // for now... 552 } 553 554 public List<TranslationUnit> generateTranslations(Resource res, String lang) { 555 List<TranslationUnit> list = new ArrayList<>(); 556 if (res instanceof StructureDefinition) { 557 StructureDefinition sd = (StructureDefinition) res; 558 generateTranslations(list, sd, lang); 559 if (res.hasUserData(UserDataNames.LANGUTILS_ORPHAN)) { 560 List<TranslationUnit> orphans = (List<TranslationUnit>) res.getUserData(UserDataNames.LANGUTILS_ORPHAN); 561 for (TranslationUnit t : orphans) { 562 if (!hasInList(list, t.getId(), t.getSrcText())) { 563 list.add(new TranslationUnit(lang, "!!"+t.getId(), t.getContext(), t.getSrcText(), t.getTgtText())); 564 } 565 } 566 } 567 } else { 568 CodeSystem cs = (CodeSystem) res.getUserData(UserDataNames.LANGUTILS_SOURCE_SUPPLEMENT); 569 List<TranslationUnit> inputs = res.hasUserData(UserDataNames.LANGUTILS_SOURCE_TRANSLATIONS) ? (List<TranslationUnit>) res.getUserData(UserDataNames.LANGUTILS_SOURCE_TRANSLATIONS) : new ArrayList<>(); 570 for (ConceptDefinitionComponent cd : cs.getConcept()) { 571 generateTranslations(list, cd, lang, inputs); 572 } 573 if (cs.hasUserData(UserDataNames.LANGUTILS_ORPHAN)) { 574 List<TranslationUnit> orphans = (List<TranslationUnit>) cs.getUserData(UserDataNames.LANGUTILS_ORPHAN); 575 for (TranslationUnit t : orphans) { 576 if (!hasInList(list, t.getId(), t.getSrcText())) { 577 list.add(new TranslationUnit(lang, "!!"+t.getId(), t.getContext(), t.getSrcText(), t.getTgtText())); 578 } 579 } 580 } 581 } 582 return list; 583 } 584 585 private void generateTranslations(List<TranslationUnit> list, StructureDefinition sd, String lang) { 586 addToList(list, lang, sd, "StructureDefinition.name", "name", sd.getNameElement()); 587 addToList(list, lang, sd, "StructureDefinition.title", "title", sd.getTitleElement()); 588 addToList(list, lang, sd, "StructureDefinition.publisher", "publisher", sd.getPublisherElement()); 589 for (ContactDetail cd : sd.getContact()) { 590 addToList(list, lang, cd, "StructureDefinition.contact.name", "name", cd.getNameElement()); 591 } 592 addToList(list, lang, sd, "StructureDefinition.purpose", "purpose", sd.getPurposeElement()); 593 addToList(list, lang, sd, "StructureDefinition.copyright", "copyright", sd.getCopyrightElement()); 594 for (ElementDefinition ed : sd.getDifferential().getElement()) { 595 addToList(list, lang, ed, "StructureDefinition.element."+ed.getId()+"/label", "label", ed.getLabelElement()); 596 addToList(list, lang, ed, "StructureDefinition.element."+ed.getId()+"/short", "short", ed.getShortElement()); 597 addToList(list, lang, ed, "StructureDefinition.element."+ed.getId()+"/definition", "definition", ed.getDefinitionElement()); 598 addToList(list, lang, ed, "StructureDefinition.element."+ed.getId()+"/comment", "comment", ed.getCommentElement()); 599 addToList(list, lang, ed, "StructureDefinition.element."+ed.getId()+"/requirements", "requirements", ed.getRequirementsElement()); 600 addToList(list, lang, ed, "StructureDefinition.element."+ed.getId()+"/meaningWhenMissing", "meaningWhenMissing", ed.getMeaningWhenMissingElement()); 601 addToList(list, lang, ed, "StructureDefinition.element."+ed.getId()+"/orderMeaning", "orderMeaning", ed.getOrderMeaningElement()); 602 for (ElementDefinitionConstraintComponent con : ed.getConstraint()) { 603 addToList(list, lang, con, "StructureDefinition.element."+ed.getId()+"/constraint", "human", con.getHumanElement()); 604 } 605 if (ed.hasBinding()) { 606 addToList(list, lang, ed.getBinding(), "StructureDefinition.element."+ed.getId()+"/b/desc", "description", ed.getBinding().getDescriptionElement()); 607 for (ElementDefinitionBindingAdditionalComponent ab : ed.getBinding().getAdditional()) { 608 addToList(list, lang, ab, "StructureDefinition.element."+ed.getId()+"/ab/doco", "documentation", ab.getDocumentationElement()); 609 addToList(list, lang, ab, "StructureDefinition.element."+ed.getId()+"/ab/short", "shortDoco", ab.getShortDocoElement()); 610 } 611 } 612 } 613 } 614 615 private void addToList(List<TranslationUnit> list, String lang, Base ctxt, String name, String propName, DataType value) { 616 if (value != null && value.hasPrimitiveValue()) { 617 if (!hasInList(list, name, value.primitiveValue())) { 618 list.add(new TranslationUnit(lang, name, ctxt.getNamedProperty(propName).getDefinition(), value.primitiveValue(), value.getTranslation(lang))); 619 } 620 } 621 622 } 623 624 private void generateTranslations(List<TranslationUnit> list, ConceptDefinitionComponent cd, String lang, List<TranslationUnit> inputs) { 625 // we generate translation units for the display, the definition, and any designations and extensions that we find 626 // the id of the designation is the use.code (there will be a use) and for the extension, the tail of the extension URL 627 // todo: do we need to worry about name clashes? why would we, and more importantly, how would we solve that? 628 629 addTranslationUnit(list, cd.getCode(), cd.getDisplay(), lang, inputs); 630 if (cd.hasDefinition()) { 631 addTranslationUnit(list, cd.getCode()+"@definition", cd.getDefinition(), lang, inputs); 632 } 633 for (ConceptDefinitionDesignationComponent d : cd.getDesignation()) { 634 addTranslationUnit(list, cd.getCode()+"@"+d.getUse().getCode(), d.getValue(), lang, inputs); 635 } 636 for (Extension e : cd.getExtension()) { 637 addTranslationUnit(list, cd.getCode()+"@"+tail(e.getUrl()), e.getValue().primitiveValue(), lang, inputs); 638 } 639 } 640 641 private void addTranslationUnit(List<TranslationUnit> list, String id, String srcText, String lang, List<TranslationUnit> inputs) { 642 TranslationUnit existing = null; 643 for (TranslationUnit t : inputs) { 644 if (id.equals(t.getId())) { 645 existing = t; 646 break; 647 } 648 } 649 if (!hasInList(list, id, srcText)) { 650 // not sure what to do with context? 651 if (existing == null) { 652 list.add(new TranslationUnit(lang, id, null, srcText, null)); 653 } else if (srcText.equals(existing.getSrcText())) { 654 list.add(new TranslationUnit(lang, id, null, srcText, existing.getTgtText())); 655 } else { 656 list.add(new TranslationUnit(lang, id, null, srcText, "!!"+existing.getTgtText()).setOriginal(existing.getSrcText())); 657 } 658 } 659 } 660 661 private String getDefinition(ConceptDefinitionComponent cd) { 662 ConceptPropertyComponent v = CodeSystemUtilities.getProperty(cd, "translation-context"); 663 if (v != null && v.hasValue()) { 664 return v.getValue().primitiveValue(); 665 } else { 666 return cd.getDefinition(); 667 } 668 } 669 670 public List<TranslationUnit> generateTranslations(Element e, String lang) { 671 TranslationUnitCollection list = new TranslationUnitCollection(); 672 generateTranslations(e, lang, list, e.fhirType()); 673 return list.list; 674 } 675 676 private void generateTranslations(Element e, String lang, TranslationUnitCollection list, String path) { 677 String npath = pathForElement(path, e); 678 if ((e.getProperty().isTranslatable() || isTranslatable(e.getProperty().getDefinition().getBase().getPath())) 679 && !isExemptFromTranslations(e.getProperty().getDefinition().getBase().getPath())) { 680 String id = e.getProperty().getDefinition().getBase().getPath(); // .getProperty().getDefinition().getPath(); 681 String context = e.getProperty().getDefinition().getDefinition(); 682 String src = e.primitiveValue(); 683 String tgt = getTranslation(e, lang); 684 if (!hasInList(list.list, id, src)) { 685 list.add(new TranslationUnit(lang, id, context, src, tgt)); 686 } 687 } 688 if (e.hasChildren()) { 689 for (Element c : e.getChildren()) { 690 generateTranslations(c, lang, list, npath); 691 } 692 } 693 } 694 695 private boolean hasInList(List<TranslationUnit> list, String id, String src) { 696 for (TranslationUnit t : list) { 697 if (t.getId() != null && t.getId().equals(id) && t.getSrcText()!= null && t.getSrcText().equals(src)) { 698 return true; 699 } 700 } 701 return false; 702 } 703 704 /** 705 * override specifications 706 * 707 * @param path 708 * @return 709 */ 710 private boolean isTranslatable(String path) { 711 return Utilities.existsInList(path, "TestCases.publisher", 712 "TestCases.contact.telecom.value", 713 "TestCases.definition", 714 "TestCases.parameter.name", 715 "TestCases.parameter.description", 716 "TestCases.scope.description ", 717 "TestCases.dependency.description", 718 "TestCases.mode.description", 719 "TestCases.suite", 720 "TestCases.suite.name", 721 "TestCases.suite.description", 722 "TestCases.suite.test", 723 "TestCases.suite.test.name", 724 "TestCases.suite.test.description", 725 "TestCases.suite.test.assert.human", 726 "ActorDefinition.title", 727 "ActorDefinition.description", 728 "ActorDefinition.purpose", 729 "ActorDefinition.copyright", 730 "ActorDefinition.copyrightLabel", 731 "ActorDefinition.documentation", 732 "Requirements.title", 733 "Requirements.publisher", 734 "Requirements.description", 735 "Requirements.purpose", 736 "Requirements.copyright", 737 "Requirements.copyrightLabel", 738 "Requirements.statement.label", 739 "Requirements.statement.requirement"); 740 } 741 742 private boolean isExemptFromTranslations(String path) { 743 if (path.endsWith(".reference")) { 744 return true; 745 } 746 return Utilities.existsInList(path, 747 "ImplementationGuide.definition.parameter.value", "ImplementationGuide.dependsOn.version", "ImplementationGuide.dependsOn.id", 748 "CanonicalResource.name", 749 "CapabilityStatement.rest.resource.searchRevInclude", "CapabilityStatement.rest.resource.searchInclude", "CapabilityStatement.rest.resource.searchParam.name", 750 "SearchParameter.expression", "SearchParameter.xpath", 751 "ExampleScenario.actor.actorId", "ExampleScenario.instance.resourceId", "ExampleScenario.instance.containedInstance.resourceId", "ExampleScenario.instance.version.versionId", 752 "ExampleScenario.process.step.operation.number", "ExampleScenario.process.step.operation.initiator", "ExampleScenario.process.step.operation.receiver", 753 "ExampleScenario.process.step.operation.number", "ExampleScenario.process.step.operation.initiator", "ExampleScenario.process.step.operation.receiver", 754 "OperationDefinition.parameter.max", "OperationDefinition.overload.parameterName", 755 "StructureMap.group.rule.source.type", "StructureMap.group.rule.source.element", "StructureMap.group.rule.target.element"); 756 } 757 758 private String getTranslation(Element e, String lang) { 759 if (!e.hasChildren()) { 760 return null; 761 } 762 for (Element ext : e.getChildren()) { 763 if ("Extension".equals(ext.fhirType()) && "http://hl7.org/fhir/StructureDefinition/translation".equals(ext.getNamedChildValue("url"))) { 764 String l = null; 765 String v = null; 766 for (Element subExt : ext.getChildren()) { 767 if ("Extension".equals(subExt.fhirType()) && "lang".equals(subExt.getNamedChildValue("url"))) { 768 l = subExt.getNamedChildValue("value"); 769 } 770 if ("Extension".equals(subExt.fhirType()) && "content".equals(subExt.getNamedChildValue("url"))) { 771 v = subExt.getNamedChildValue("value"); 772 } 773 } 774 if (lang.equals(l)) { 775 return v; 776 } 777 } 778 } 779 return null; 780 } 781 782 public boolean switchLanguage(Base r, String lang, boolean markLanguage, boolean contained) { 783 boolean changed = false; 784 if (r.isPrimitive()) { 785 786 PrimitiveType<?> dt = (PrimitiveType<?>) r; 787 String cnt = ExtensionUtilities.getLanguageTranslation(dt, lang); 788 dt.removeExtension(ExtensionDefinitions.EXT_TRANSLATION); 789 if (cnt != null) { 790 dt.setValueAsString(cnt); 791 changed = true; 792 } 793 } 794 795 if (r.fhirType().equals("Narrative")) { 796 Base div = r.getChildValueByName("div"); 797 Base status = r.getChildValueByName("status"); 798 799 XhtmlNode xhtml = div.getXhtml(); 800 xhtml = adjustToLang(xhtml, lang, status == null ? null : status.primitiveValue()); 801 if (xhtml == null) { 802 r.removeChild("div", div); 803 } else { 804 div.setXhtml(xhtml); 805 } 806 } 807 for (Property p : r.children()) { 808 for (Base c : p.getValues()) { 809 changed = switchLanguage(c, lang, markLanguage, p.getName().equals("contained")) || changed; 810 } 811 } 812 if (markLanguage && r.isResource() && !contained) { 813 Resource res = (Resource) r; 814 res.setLanguage(lang); 815 changed = true; 816 } 817 return changed; 818 } 819 820 private XhtmlNode adjustToLang(XhtmlNode xhtml, String lang, String status) { 821 if (xhtml == null) { 822 return null; 823 } 824 //see if there's a language specific section 825 for (XhtmlNode div : xhtml.getChildNodes()) { 826 if ("div".equals(div.getName())) { 827 String l = div.hasAttribute("lang") ? div.getAttribute("lang") : div.getAttribute("xml:lang"); 828 if (lang.equals(l)) { 829 return div; 830 } 831 } 832 } 833 // if the root language is correct 834 // this isn't first because a narrative marked for one language might have other language subsections 835 String l = xhtml.hasAttribute("lang") ? xhtml.getAttribute("lang") : xhtml.getAttribute("xml:lang"); 836 if (lang.equals(l)) { 837 return xhtml; 838 } 839 // if the root language is null and the status is not additional 840 // it can be regenerated 841 if (l == null && Utilities.existsInList(status, "generated", "extensions")) { 842 return null; 843 } 844 // well, really, not much we can do... 845 return xhtml; 846 } 847 848 public boolean switchLanguage(Element e, String lang, boolean markLanguage) throws IOException { 849 boolean changed = false; 850 if (e.getProperty().isTranslatable()) { 851 String cnt = getTranslation(e, lang); 852 e.removeExtension(ExtensionDefinitions.EXT_TRANSLATION); 853 if (cnt != null) { 854 e.setValue(cnt); 855 changed = true; 856 } 857 } 858 if (e.fhirType().equals("Narrative") && e.hasChild("div")) { 859 XhtmlNode xhtml = e.getNamedChild("div").getXhtml(); 860 xhtml = adjustToLang(xhtml, lang, e.getNamedChildValue("status")); 861 if (xhtml == null) { 862 e.removeChild("div"); 863 } else { 864 e.getNamedChild("div").setXhtml(xhtml); 865 } 866 } 867 if (e.hasChildren()) { 868 for (Element c : e.getChildren()) { 869 changed = switchLanguage(c, lang, markLanguage) || changed; 870 } 871 } 872 if (markLanguage && e.isResource() && e.getSpecial() != SpecialElement.CONTAINED) { 873 e.setChildValue("language", lang); 874 changed = true; 875 } 876 return changed; 877 } 878 879 public boolean hasTranslation(org.hl7.fhir.r5.model.Element e, String lang) { 880 return getTranslation(e, lang) != null; 881 } 882 883 public String getTranslation(org.hl7.fhir.r5.model.Element e, String lang) { 884 for (Extension ext : e.getExtensionsByUrl(ExtensionDefinitions.EXT_TRANSLATION)) { 885 String l = ext.getExtensionString("lang"); 886 String v = ext.getExtensionString("content"); 887 if (langsMatch(l, lang) && v != null) { 888 return v; 889 } 890 } 891 return null; 892 } 893 894 public String getTranslationOrBase(PrimitiveType<?> e, String lang) { 895 for (Extension ext : e.getExtensionsByUrl(ExtensionDefinitions.EXT_TRANSLATION)) { 896 String l = ext.getExtensionString("lang"); 897 String v = ext.getExtensionString("content"); 898 if (langsMatch(l, lang) && v != null) { 899 return v; 900 } 901 } 902 return e.primitiveValue(); 903 } 904 905 public Element copyToLanguage(Element element, String lang, boolean markLanguage) throws IOException { 906 Element result = (Element) element.copy(); 907 switchLanguage(result, lang, markLanguage); 908 return result; 909 } 910 911 public Resource copyToLanguage(Resource res, String lang, boolean markLanguage) { 912 if (res == null) { 913 return null; 914 } 915 Resource r = res.copy(); 916 switchLanguage(r, lang, markLanguage, false); 917 return r; 918 } 919}