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