001package org.hl7.fhir.r5.elementmodel;
002
003import java.util.ArrayList;
004import java.util.Arrays;
005import java.util.HashSet;
006import java.util.List;
007import java.util.Set;
008
009import org.checkerframework.checker.units.qual.cd;
010import org.hl7.fhir.r5.context.ContextUtilities;
011import org.hl7.fhir.r5.context.IWorkerContext;
012import org.hl7.fhir.r5.model.CodeSystem;
013import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
014import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent;
015import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent;
016import org.hl7.fhir.r5.model.DataType;
017import org.hl7.fhir.r5.model.Resource;
018import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
019import org.hl7.fhir.utilities.TextFile;
020import org.hl7.fhir.utilities.Utilities;
021import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader;
022import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader.LanguagePreference;
023import org.hl7.fhir.utilities.i18n.LanguageFileProducer;
024import org.hl7.fhir.utilities.i18n.LanguageFileProducer.LanguageProducerLanguageSession;
025import org.hl7.fhir.utilities.i18n.LanguageFileProducer.TextUnit;
026import org.hl7.fhir.utilities.i18n.LanguageFileProducer.TranslationUnit;
027
028/**
029 * in here:
030 *   * generateTranslations
031 *   * importFromTranslations
032 *   * stripTranslations
033 *   * switchLanguage
034 *   
035 *  in the validator
036 *  
037 * @author grahamegrieve
038 *  generateTranslations = -langTransform export -src {src} -tgt {tgt} -dest {dest}
039 *  importFromTranslations =  -langTransform import -src {src} -tgt {tgt} -dest {dest}
040 */
041public class LanguageUtils {
042
043  public static final List<String> TRANSLATION_SUPPLEMENT_RESOURCE_TYPES = Arrays.asList("CodeSystem", "StructureDefinition", "Questionnaire");
044
045  private static final String ORPHAN_TRANSLATIONS_NAME = "translations.orphans";
046
047  private static final String SUPPLEMENT_NAME = "translations.supplement";
048
049  IWorkerContext context;
050  private List<String> crlist;
051  
052  
053  public LanguageUtils(IWorkerContext context) {
054    super();
055    this.context = context;
056  }
057
058  public void generateTranslations(Element resource, LanguageProducerLanguageSession session) {
059    translate(null, resource, session);
060  }
061  
062  
063  private void translate(Element parent, Element element, LanguageProducerLanguageSession langSession) {
064    if (element.isPrimitive() && isTranslatable(element)) {
065      String base = element.primitiveValue();
066      if (base != null) {
067        String translation = getSpecialTranslation(parent, element, langSession.getTargetLang());
068        if (translation == null) {
069          translation = element.getTranslation(langSession.getTargetLang());
070        }
071        langSession.entry(new TextUnit(pathForElement(element), contextForElement(element), base, translation));
072      }
073    }
074    for (Element c: element.getChildren()) {
075      if (!c.getName().equals("designation")) {
076        translate(element, c, langSession);
077      }
078    }
079  }
080  
081
082  private String contextForElement(Element element) {
083    throw new Error("Not done yet");
084  }
085
086  private String getSpecialTranslation(Element parent, Element element, String targetLang) {
087    if (parent == null) {
088      return null;
089    }
090    if (Utilities.existsInList(pathForElement(parent), "CodeSystem.concept", "CodeSystem.concept.concept") && "CodeSystem.concept.display".equals(pathForElement(element))) {
091      return getDesignationTranslation(parent, targetLang);
092    }
093    if (Utilities.existsInList(pathForElement(parent), "ValueSet.compose.include.concept") && "ValueSet.compose.include.concept.display".equals(pathForElement(element))) {
094      return getDesignationTranslation(parent, targetLang);
095    }
096    if (Utilities.existsInList(pathForElement(parent), "ValueSet.expansion.contains", "ValueSet.expansion.contains.contains") && "ValueSet.expansion.contains.display".equals(pathForElement(element))) {
097      return getDesignationTranslation(parent, targetLang);
098    }
099    return null;
100  }
101
102  private String getDesignationTranslation(Element parent, String targetLang) {
103    for (Element e : parent.getChildren("designation")) {
104      String lang = e.getNamedChildValue("language");
105      if (langsMatch(targetLang, lang)) {
106        return e.getNamedChildValue("value");
107      }
108    }
109    return null;
110  }
111
112  private boolean isTranslatable(Element element) {    
113    return element.getProperty().isTranslatable() && !Utilities.existsInList(pathForElement(element), "CanonicalResource.version");
114  }
115
116  private String pathForElement(Element element) {
117    // special case support for metadata elements prior to R5:
118    if (crlist == null) {
119      crlist = new ContextUtilities(context).getCanonicalResourceNames();
120    }
121    String bp = element.getBasePath();
122    if (crlist.contains(element.getProperty().getStructure().getType())) {
123      String fp = bp.replace(element.getProperty().getStructure().getType()+".", "CanonicalResource.");
124      if (Utilities.existsInList(fp,
125         "CanonicalResource.url", "CanonicalResource.identifier", "CanonicalResource.version", "CanonicalResource.name", 
126         "CanonicalResource.title", "CanonicalResource.status", "CanonicalResource.experimental", "CanonicalResource.date",
127         "CanonicalResource.publisher", "CanonicalResource.contact", "CanonicalResource.description", "CanonicalResource.useContext", 
128         "CanonicalResource.jurisdiction"))  {
129        return fp;
130      }
131    }
132    return bp; 
133  }
134  
135  public int importFromTranslations(Element resource, Set<TranslationUnit> translations) {
136    return importFromTranslations(null, resource, translations);
137  }
138  
139  private int importFromTranslations(Element parent, Element element, Set<TranslationUnit> translations) {
140    int t = 0;
141    if (element.isPrimitive() && isTranslatable(element)) {
142      String base = element.primitiveValue();
143      if (base != null) {
144        Set<TranslationUnit> tlist = findTranslations(pathForElement(element), base, translations);
145        for (TranslationUnit translation : tlist) {
146          t++;
147          if (!handleAsSpecial(parent, element, translation)) {
148            element.setTranslation(translation.getLanguage(), translation.getTgtText());
149          }
150        }
151      }
152    }
153    for (Element c: element.getChildren()) {
154      if (!c.getName().equals("designation")) {
155        t = t + importFromTranslations(element, c, translations);
156      }
157    }
158    return t;
159  }
160
161  private boolean handleAsSpecial(Element parent, Element element, TranslationUnit translation) {
162    if (parent == null) {
163      return false;
164    }
165    if (Utilities.existsInList(pathForElement(parent), "CodeSystem.concept", "CodeSystem.concept.concept") && "CodeSystem.concept.display".equals(pathForElement(element))) {
166      return setDesignationTranslation(parent, translation.getLanguage(), translation.getTgtText());
167    }
168    if (Utilities.existsInList(pathForElement(parent), "ValueSet.compose.include.concept") && "ValueSet.compose.include.concept.display".equals(pathForElement(element))) {
169      return setDesignationTranslation(parent, translation.getLanguage(), translation.getTgtText());
170    }
171    if (Utilities.existsInList(pathForElement(parent), "ValueSet.expansion.contains", "ValueSet.expansion.contains.contains") && "ValueSet.expansion.contains.display".equals(pathForElement(element))) {
172      return setDesignationTranslation(parent, translation.getLanguage(), translation.getTgtText());
173    }
174    return false;
175  }
176
177  private boolean setDesignationTranslation(Element parent, String targetLang, String translation) {
178    for (Element e : parent.getChildren("designation")) {
179      String lang = e.getNamedChildValue("language");
180      if (langsMatch(targetLang, lang)) {
181        Element value = e.getNamedChild("value");
182        if (value != null) {
183          value.setValue(translation);
184        } else {
185          e.addElement("value").setValue(translation);
186        }
187        return true;
188      }
189    }
190    Element d = parent.addElement("designation");
191    d.addElement("language").setValue(targetLang);
192    d.addElement("value").setValue(translation);
193    return true;
194  }
195
196  private Set<TranslationUnit> findTranslations(String path, String src, Set<TranslationUnit> translations) {
197    Set<TranslationUnit> res = new HashSet<>();
198    for (TranslationUnit translation : translations) {
199      if (path.equals(translation.getId()) && src.equals(translation.getSrcText())) {
200        res.add(translation);
201      }
202    }
203    return res;
204  }
205
206  public static boolean langsMatchExact(AcceptLanguageHeader langs, String srcLang) {
207    if (langs == null) {
208      return false;
209    }
210    for (LanguagePreference lang : langs.getLangs()) {
211      if (lang.getValue() > 0) {
212        if ("*".equals(lang.getLang())) {
213          return true;
214        } else {
215          return langsMatch(lang.getLang(), srcLang);
216        }
217      }
218    }
219    return false;
220  }
221
222  public static boolean langsMatch(AcceptLanguageHeader langs, String srcLang) {
223    if (langs == null) {
224      return false;
225    }
226    for (LanguagePreference lang : langs.getLangs()) {
227      if (lang.getValue() > 0) {
228        if ("*".equals(lang.getLang())) {
229          return true;
230        } else {
231          boolean ok = langsMatch(lang.getLang(), srcLang);
232          if (ok) {
233            return true;
234          }
235        }
236      }
237    }
238    return false;
239  }
240
241  public static boolean langsMatchExact(String dstLang, String srcLang) {
242    return dstLang == null ? false : dstLang.equals(srcLang);
243  }
244
245  public static boolean langsMatch(String dstLang, String srcLang) {
246    return dstLang == null || srcLang == null ? false : dstLang.startsWith(srcLang) || "*".equals(srcLang);
247  }
248
249  public static void fillSupplement(CodeSystem cs, List<TranslationUnit> list) {
250    cs.setUserData(SUPPLEMENT_NAME, "true");
251    for (TranslationUnit tu : list) {
252      ConceptDefinitionComponent cd = CodeSystemUtilities.getCode(cs, tu.getId());
253      if (cd != null && cd.hasDisplay() && cd.getDisplay().equals(tu.getSrcText())) {
254        cd.addDesignation().setLanguage(tu.getLanguage()).setValue(tu.getTgtText());
255      } else {
256        addOrphanTranslation(cs, tu);
257      }
258    }    
259  }
260
261  private static void addOrphanTranslation(CodeSystem cs, TranslationUnit tu) {
262    List<TranslationUnit> list = (List<TranslationUnit>) cs.getUserData(ORPHAN_TRANSLATIONS_NAME);
263    if (list == null) {
264      list = new ArrayList<>();
265      cs.setUserData(ORPHAN_TRANSLATIONS_NAME, list);
266    }
267    list.add(tu);
268  }
269
270  public static String nameForLang(String lang) {
271    // todo: replace with structures from loading languages properly
272    switch (lang) {
273    case "en" : return "English";
274    case "de" : return "German";
275    case "es" : return "Spanish";
276    case "nl" : return "Dutch";
277    }
278    return Utilities.capitalize(lang);
279  }
280
281  public static String titleForLang(String lang) {
282    // todo: replace with structures from loading languages properly
283    switch (lang) {
284    case "en" : return "English";
285    case "de" : return "German";
286    case "es" : return "Spanish";
287    case "nl" : return "Dutch";
288    }
289    return Utilities.capitalize(lang);
290  }
291
292  public static boolean handlesAsResource(Resource resource) {
293    return (resource instanceof CodeSystem && resource.hasUserData(SUPPLEMENT_NAME));
294  }
295
296  public static boolean handlesAsElement(Element element) {
297    return false; // for now...
298  }
299
300  public static List<TranslationUnit> generateTranslations(Resource res, String lang) {
301    List<TranslationUnit> list = new ArrayList<>();
302    CodeSystem cs = (CodeSystem) res;
303    for (ConceptDefinitionComponent cd : cs.getConcept()) {
304      generateTranslations(list, cd, lang);
305    }
306    return list;
307  }
308
309  private static void generateTranslations(List<TranslationUnit> list, ConceptDefinitionComponent cd, String lang) {
310    String code = cd.getCode();
311    String display = cd.getDisplay();
312    String target = null;
313    for (ConceptDefinitionDesignationComponent d : cd.getDesignation()) {
314      if (target == null && !d.hasUse() && d.hasLanguage() && lang.equals(d.getLanguage())) {
315        target = d.getValue();
316      }
317    }
318    for (ConceptDefinitionDesignationComponent d : cd.getDesignation()) {
319      if (target == null && d.hasLanguage() && lang.equals(d.getLanguage())) {
320        target = d.getValue();
321      }
322    }
323    list.add(new TranslationUnit(lang, code, getDefinition(cd), display, target));
324    for (ConceptDefinitionComponent cd1 : cd.getConcept()) {
325      generateTranslations(list, cd1, lang);
326    }
327  }
328
329  private static String getDefinition(ConceptDefinitionComponent cd) {
330    ConceptPropertyComponent v = CodeSystemUtilities.getProperty(cd, "translation-context");
331    if (v != null && v.hasValue()) {
332      return v.getValue().primitiveValue();
333    } else {
334      return cd.getDefinition();
335    }
336  }
337}