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}