001package org.hl7.fhir.r4.elementmodel;
002
003import java.io.IOException;
004import java.io.InputStream;
005import java.io.OutputStream;
006import java.util.HashSet;
007import java.util.List;
008import java.util.Set;
009
010import org.hl7.fhir.r4.context.IWorkerContext;
011import org.hl7.fhir.r4.elementmodel.Element.SpecialElement;
012import org.hl7.fhir.r4.formats.IParser.OutputStyle;
013import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent;
014import org.hl7.fhir.r4.model.StructureDefinition;
015import org.hl7.fhir.r4.utils.SnomedExpressions;
016import org.hl7.fhir.r4.utils.SnomedExpressions.Expression;
017import org.hl7.fhir.r4.utils.formats.Turtle;
018import org.hl7.fhir.r4.utils.formats.Turtle.Complex;
019import org.hl7.fhir.r4.utils.formats.Turtle.Section;
020import org.hl7.fhir.r4.utils.formats.Turtle.Subject;
021import org.hl7.fhir.r4.utils.formats.Turtle.TTLComplex;
022import org.hl7.fhir.r4.utils.formats.Turtle.TTLList;
023import org.hl7.fhir.r4.utils.formats.Turtle.TTLLiteral;
024import org.hl7.fhir.r4.utils.formats.Turtle.TTLObject;
025import org.hl7.fhir.r4.utils.formats.Turtle.TTLURL;
026import org.hl7.fhir.exceptions.DefinitionException;
027import org.hl7.fhir.exceptions.FHIRException;
028import org.hl7.fhir.exceptions.FHIRFormatError;
029import org.hl7.fhir.utilities.TextFile;
030import org.hl7.fhir.utilities.Utilities;
031import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
032import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
033
034
035public class TurtleParser extends ParserBase {
036
037  private String base;
038
039  public static String FHIR_URI_BASE = "http://hl7.org/fhir/";
040  public static String FHIR_VERSION_BASE = "http://build.fhir.org/";
041
042  public TurtleParser(IWorkerContext context) {
043    super(context);
044  }
045  @Override
046  public Element parse(InputStream input) throws IOException, FHIRException {
047    Turtle src = new Turtle();
048    if (policy == ValidationPolicy.EVERYTHING) {
049      try {
050        src.parse(TextFile.streamToString(input));
051      } catch (Exception e) {  
052        logError(-1, -1, "(document)", IssueType.INVALID, "Error parsing Turtle: "+e.getMessage(), IssueSeverity.FATAL);
053        return null;
054      }
055      return parse(src);  
056    } else {
057    src.parse(TextFile.streamToString(input));
058      return parse(src);  
059    } 
060  }
061  
062  private Element parse(Turtle src) throws FHIRException {
063    // we actually ignore the stated URL here
064    for (TTLComplex cmp : src.getObjects().values()) {
065      for (String p : cmp.getPredicates().keySet()) {
066        if ((FHIR_URI_BASE + "nodeRole").equals(p) && cmp.getPredicates().get(p).hasValue(FHIR_URI_BASE + "treeRoot")) {
067          return parse(src, cmp);
068        }
069      }
070    }
071    // still here: well, we didn't find a start point
072    String msg = "Error parsing Turtle: unable to find any node maked as the entry point (where " + FHIR_URI_BASE + "nodeRole = " + FHIR_URI_BASE + "treeRoot)";
073    if (policy == ValidationPolicy.EVERYTHING) {
074      logError(-1, -1, "(document)", IssueType.INVALID, msg, IssueSeverity.FATAL);
075      return null;
076    } else {
077      throw new FHIRFormatError(msg);
078    } 
079  }
080  
081  private Element parse(Turtle src, TTLComplex cmp) throws FHIRException {
082    TTLObject type = cmp.getPredicates().get("http://www.w3.org/2000/01/rdf-schema#type");
083    if (type == null) {
084      logError(cmp.getLine(), cmp.getCol(), "(document)", IssueType.INVALID, "Unknown resource type (missing rdfs:type)", IssueSeverity.FATAL);
085      return null;
086    }
087    if (type instanceof TTLList) {
088      // this is actually broken - really we have to look through the structure definitions at this point
089      for (TTLObject obj : ((TTLList) type).getList()) {
090        if (obj instanceof TTLURL && ((TTLURL) obj).getUri().startsWith(FHIR_URI_BASE)) {
091          type = obj;
092          break;
093        }
094      }
095    }
096    if (!(type instanceof TTLURL)) {
097      logError(cmp.getLine(), cmp.getCol(), "(document)", IssueType.INVALID, "Unexpected datatype for rdfs:type)", IssueSeverity.FATAL);
098      return null;
099    }
100    String name = ((TTLURL) type).getUri();
101    String ns = name.substring(0, name.lastIndexOf("/"));
102    name = name.substring(name.lastIndexOf("/")+1);
103    String path = "/"+name;
104
105    StructureDefinition sd = getDefinition(cmp.getLine(), cmp.getCol(), ns, name);
106    if (sd == null)
107      return null;
108
109    Element result = new Element(name, new Property(context, sd.getSnapshot().getElement().get(0), sd));
110    result.markLocation(cmp.getLine(), cmp.getCol());
111    result.setType(name);
112    parseChildren(src, path, cmp, result, false);
113    result.numberChildren();
114    return result;  
115  }
116  
117  private void parseChildren(Turtle src, String path, TTLComplex object, Element context, boolean primitive) throws FHIRException {
118
119    List<Property> properties = context.getProperty().getChildProperties(context.getName(), null);
120    Set<String> processed = new HashSet<String>();
121    if (primitive)
122      processed.add(FHIR_URI_BASE + "value");
123
124    // note that we do not trouble ourselves to maintain the wire format order here - we don't even know what it was anyway
125    // first pass: process the properties
126    for (Property property : properties) {
127      if (property.isChoice()) {
128        for (TypeRefComponent type : property.getDefinition().getType()) {
129          String eName = property.getName().substring(0, property.getName().length()-3) + Utilities.capitalize(type.getCode());
130          parseChild(src, object, context, processed, property, path, getFormalName(property, eName));
131        }
132      } else  {
133        parseChild(src, object, context, processed, property, path, getFormalName(property));
134      } 
135    }
136
137    // second pass: check for things not processed
138    if (policy != ValidationPolicy.NONE) {
139      for (String u : object.getPredicates().keySet()) {
140        if (!processed.contains(u)) {
141          TTLObject n = object.getPredicates().get(u);
142          logError(n.getLine(), n.getCol(), path, IssueType.STRUCTURE, "Unrecognised predicate '"+u+"'", IssueSeverity.ERROR);         
143        }
144      }
145    }
146  }
147  
148  private void parseChild(Turtle src, TTLComplex object, Element context, Set<String> processed, Property property, String path, String name) throws FHIRException {
149    processed.add(name);
150    String npath = path+"/"+property.getName();
151    TTLObject e = object.getPredicates().get(FHIR_URI_BASE + name);
152    if (e == null)
153      return;
154    if (property.isList() && (e instanceof TTLList)) {
155      TTLList arr = (TTLList) e;
156      for (TTLObject am : arr.getList()) {
157        parseChildInstance(src, npath, object, context, property, name, am);
158      }
159    } else {
160      parseChildInstance(src, npath, object, context, property, name, e);
161    }
162  }
163
164  private void parseChildInstance(Turtle src, String npath, TTLComplex object, Element context, Property property, String name, TTLObject e) throws FHIRException {
165    if (property.isResource())
166      parseResource(src, npath, object, context, property, name, e);
167    else  if (e instanceof TTLComplex) {
168      TTLComplex child = (TTLComplex) e;
169      Element n = new Element(tail(name), property).markLocation(e.getLine(), e.getCol());
170      context.getChildren().add(n);
171      if (property.isPrimitive(property.getType(tail(name)))) {
172        parseChildren(src, npath, child, n, true);
173        TTLObject val = child.getPredicates().get(FHIR_URI_BASE + "value");
174        if (val != null) {
175          if (val instanceof TTLLiteral) {
176            String value = ((TTLLiteral) val).getValue();
177            String type = ((TTLLiteral) val).getType();
178            // todo: check type
179            n.setValue(value);
180          } else
181            logError(object.getLine(), object.getCol(), npath, IssueType.INVALID, "This property must be a Literal, not a "+e.getClass().getName(), IssueSeverity.ERROR);
182        }
183      } else 
184        parseChildren(src, npath, child, n, false);
185
186    } else 
187      logError(object.getLine(), object.getCol(), npath, IssueType.INVALID, "This property must be a URI or bnode, not a "+e.getClass().getName(), IssueSeverity.ERROR);
188  }
189
190
191  private String tail(String name) {
192    return name.substring(name.lastIndexOf(".")+1);
193  }
194
195  private void parseResource(Turtle src, String npath, TTLComplex object, Element context, Property property, String name, TTLObject e) throws FHIRException {
196    TTLComplex obj;
197    if (e instanceof TTLComplex) 
198      obj = (TTLComplex) e;
199    else if (e instanceof TTLURL) {
200      String url = ((TTLURL) e).getUri();
201      obj = src.getObject(url);
202      if (obj == null) {
203        logError(e.getLine(), e.getCol(), npath, IssueType.INVALID, "reference to "+url+" cannot be resolved", IssueSeverity.FATAL);
204        return;
205      }
206    } else
207      throw new FHIRFormatError("Wrong type for resource");
208      
209    TTLObject type = obj.getPredicates().get("http://www.w3.org/2000/01/rdf-schema#type");
210    if (type == null) {
211      logError(object.getLine(), object.getCol(), npath, IssueType.INVALID, "Unknown resource type (missing rdfs:type)", IssueSeverity.FATAL);
212      return;
213  }
214    if (type instanceof TTLList) {
215      // this is actually broken - really we have to look through the structure definitions at this point
216      for (TTLObject tobj : ((TTLList) type).getList()) {
217        if (tobj instanceof TTLURL && ((TTLURL) tobj).getUri().startsWith(FHIR_URI_BASE)) {
218          type = tobj;
219          break;
220        }
221      }
222    }
223    if (!(type instanceof TTLURL)) {
224      logError(object.getLine(), object.getCol(), npath, IssueType.INVALID, "Unexpected datatype for rdfs:type)", IssueSeverity.FATAL);
225      return;
226    }
227    String rt = ((TTLURL) type).getUri();
228    String ns = rt.substring(0, rt.lastIndexOf("/"));
229    rt = rt.substring(rt.lastIndexOf("/")+1);
230    
231    StructureDefinition sd = getDefinition(object.getLine(), object.getCol(), ns, rt);
232    if (sd == null)
233      return;
234    
235    Element n = new Element(tail(name), property).markLocation(object.getLine(), object.getCol());
236    context.getChildren().add(n);
237    n.updateProperty(new Property(this.context, sd.getSnapshot().getElement().get(0), sd), SpecialElement.fromProperty(n.getProperty()), property);
238    n.setType(rt);
239    parseChildren(src, npath, obj, n, false);
240  }
241  
242  private String getFormalName(Property property) {
243    String en = property.getDefinition().getBase().getPath();
244    if (en == null) 
245      en = property.getDefinition().getPath();
246//    boolean doType = false;
247//      if (en.endsWith("[x]")) {
248//        en = en.substring(0, en.length()-3);
249//        doType = true;        
250//      }
251//     if (doType || (element.getProperty().getDefinition().getType().size() > 1 && !allReference(element.getProperty().getDefinition().getType())))
252//       en = en + Utilities.capitalize(element.getType());
253    return en;
254  }
255  
256  private String getFormalName(Property property, String elementName) {
257    String en = property.getDefinition().getBase().getPath();
258    if (en == null)
259      en = property.getDefinition().getPath();
260    if (!en.endsWith("[x]")) 
261      throw new Error("Attempt to replace element name for a non-choice type");
262    return en.substring(0, en.lastIndexOf(".")+1)+elementName;
263  }
264  
265  
266  @Override
267  public void compose(Element e, OutputStream stream, OutputStyle style, String base) throws IOException, FHIRException {
268    this.base = base;
269    
270                Turtle ttl = new Turtle();
271                compose(e, ttl, base);
272                ttl.commit(stream, false);
273  }
274
275
276
277  public void compose(Element e, Turtle ttl, String base) throws FHIRException {
278    ttl.prefix("fhir", FHIR_URI_BASE);
279    ttl.prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#");
280    ttl.prefix("owl", "http://www.w3.org/2002/07/owl#");
281    ttl.prefix("xsd", "http://www.w3.org/2001/XMLSchema#");
282
283
284    Section section = ttl.section("resource");
285    String subjId = genSubjectId(e);
286
287    String ontologyId = subjId.replace(">", ".ttl>");
288    Section ontology = ttl.section("ontology header");
289    ontology.triple(ontologyId, "a", "owl:Ontology");
290    ontology.triple(ontologyId, "owl:imports", "fhir:fhir.ttl");
291    if(ontologyId.startsWith("<" + FHIR_URI_BASE))
292      ontology.triple(ontologyId, "owl:versionIRI", ontologyId.replace(FHIR_URI_BASE, FHIR_VERSION_BASE));
293
294    Subject subject = section.triple(subjId, "a", "fhir:" + e.getType());
295                subject.linkedPredicate("fhir:nodeRole", "fhir:treeRoot", linkResolver == null ? null : linkResolver.resolvePage("rdf.html#tree-root"));
296
297                for (Element child : e.getChildren()) {
298                        composeElement(section, subject, child, null);
299                }
300
301  }
302  
303  protected String getURIType(String uri) {
304    if(uri.startsWith("<" + FHIR_URI_BASE))
305      if(uri.substring(FHIR_URI_BASE.length() + 1).contains("/"))
306        return uri.substring(FHIR_URI_BASE.length() + 1, uri.indexOf('/', FHIR_URI_BASE.length() + 1));
307    return null;
308  }
309
310  protected String getReferenceURI(String ref) {
311    if (ref != null && (ref.startsWith("http://") || ref.startsWith("https://")))
312      return "<" + ref + ">";
313    else if (base != null && ref != null && ref.contains("/"))
314      return "<" + Utilities.appendForwardSlash(base) + ref + ">";
315    else
316      return null;
317    }
318
319  protected void decorateReference(Complex t, Element coding) {
320    String refURI = getReferenceURI(coding.getChildValue("reference"));
321    if(refURI != null)
322      t.linkedPredicate("fhir:link", refURI, linkResolver == null ? null : linkResolver.resolvePage("rdf.html#reference"));
323  }
324  
325  protected void decorateCanonical(Complex t, Element canonical) {
326    String refURI = getReferenceURI(canonical.primitiveValue());
327    if(refURI != null)
328      t.linkedPredicate("fhir:link", refURI, linkResolver == null ? null : linkResolver.resolvePage("rdf.html#reference"));
329  }
330  
331  private String genSubjectId(Element e) {
332    String id = e.getChildValue("id");
333    if (base == null || id == null)
334      return "";
335    else if (base.endsWith("#"))
336      return "<" + base + e.getType() + "-" + id + ">";
337    else
338      return "<" + Utilities.pathURL(base, e.getType(), id) + ">";
339  }
340
341        private String urlescape(String s) {
342          StringBuilder b = new StringBuilder();
343          for (char ch : s.toCharArray()) {
344            if (Utilities.charInSet(ch,  ':', ';', '=', ','))
345              b.append("%"+Integer.toHexString(ch));
346            else
347              b.append(ch);
348          }
349          return b.toString();
350  }
351
352  private void composeElement(Section section, Complex ctxt, Element element, Element parent) throws FHIRException {
353//    "Extension".equals(element.getType())?
354//            (element.getProperty().getDefinition().getIsModifier()? "modifierExtension" : "extension") ; 
355    String en = getFormalName(element);
356
357          Complex t;
358          if (element.getSpecial() == SpecialElement.BUNDLE_ENTRY && parent != null && parent.getNamedChildValue("fullUrl") != null) {
359            String url = "<"+parent.getNamedChildValue("fullUrl")+">";
360            ctxt.linkedPredicate("fhir:"+en, url, linkResolver == null ? null : linkResolver.resolveProperty(element.getProperty()));
361            t = section.subject(url);
362          } else {
363            t = ctxt.linkedPredicate("fhir:"+en, linkResolver == null ? null : linkResolver.resolveProperty(element.getProperty()));
364          }
365    if (element.getSpecial() != null)
366      t.linkedPredicate("a", "fhir:"+element.fhirType(), linkResolver == null ? null : linkResolver.resolveType(element.fhirType()));
367          if (element.hasValue())
368                t.linkedPredicate("fhir:value", ttlLiteral(element.getValue(), element.getType()), linkResolver == null ? null : linkResolver.resolveType(element.getType()));
369          if (element.getProperty().isList() && (!element.isResource() || element.getSpecial() == SpecialElement.CONTAINED))
370                t.linkedPredicate("fhir:index", Integer.toString(element.getIndex()), linkResolver == null ? null : linkResolver.resolvePage("rdf.html#index"));
371
372          if ("Coding".equals(element.getType()))
373                decorateCoding(t, element, section);
374    if (Utilities.existsInList(element.getType(), "Reference"))
375      decorateReference(t, element);
376    else if (Utilities.existsInList(element.getType(), "canonical"))
377      decorateCanonical(t, element);
378                        
379    if("canonical".equals(element.getType())) {
380      String refURI = element.primitiveValue();
381      if (refURI != null) {
382        String uriType = getURIType(refURI);
383        if(uriType != null && !section.hasSubject(refURI))
384          section.triple(refURI, "a", "fhir:" + uriType);
385      }
386    }
387
388    if("Reference".equals(element.getType())) {
389      String refURI = getReferenceURI(element.getChildValue("reference"));
390      if (refURI != null) {
391        String uriType = getURIType(refURI);
392        if(uriType != null && !section.hasSubject(refURI))
393          section.triple(refURI, "a", "fhir:" + uriType);
394      }
395    }
396
397                for (Element child : element.getChildren()) {
398      if ("xhtml".equals(child.getType())) {
399        String childfn = getFormalName(child);
400        t.predicate("fhir:" + childfn, ttlLiteral(child.getValue(), child.getType()));
401      } else
402                        composeElement(section, t, child, element);
403                }
404        }
405
406  private String getFormalName(Element element) {
407    String en = null;
408    if (element.getSpecial() == null) {
409      if (element.getProperty().getDefinition().hasBase())
410        en = element.getProperty().getDefinition().getBase().getPath();
411    }
412    else if (element.getSpecial() == SpecialElement.BUNDLE_ENTRY)
413      en = "Bundle.entry.resource";
414    else if (element.getSpecial() == SpecialElement.BUNDLE_OUTCOME)
415      en = "Bundle.entry.response.outcome";
416    else if (element.getSpecial() == SpecialElement.PARAMETER)
417      en = element.getElementProperty().getDefinition().getPath();
418    else // CONTAINED
419      en = "DomainResource.contained";
420
421    if (en == null)
422      en = element.getProperty().getDefinition().getPath();
423    boolean doType = false;
424      if (en.endsWith("[x]")) {
425        en = en.substring(0, en.length()-3);
426        doType = true;
427      }
428     if (doType || (element.getProperty().getDefinition().getType().size() > 1 && !allReference(element.getProperty().getDefinition().getType())))
429       en = en + Utilities.capitalize(element.getType());
430    return en;
431  }
432
433        private boolean allReference(List<TypeRefComponent> types) {
434          for (TypeRefComponent t : types) {
435            if (!t.getCode().equals("Reference"))
436              return false;
437          }
438    return true;
439  }
440
441  static public String ttlLiteral(String value, String type) {
442          String xst = "";
443          if (type.equals("boolean"))
444            xst = "^^xsd:boolean";
445    else if (type.equals("integer"))
446      xst = "^^xsd:integer";
447    else if (type.equals("unsignedInt"))
448      xst = "^^xsd:nonNegativeInteger";
449    else if (type.equals("positiveInt"))
450      xst = "^^xsd:positiveInteger";
451    else if (type.equals("decimal"))
452      xst = "^^xsd:decimal";
453    else if (type.equals("base64Binary"))
454      xst = "^^xsd:base64Binary";
455    else if (type.equals("instant"))
456      xst = "^^xsd:dateTime";
457    else if (type.equals("time"))
458      xst = "^^xsd:time";
459    else if (type.equals("date") || type.equals("dateTime") ) {
460      String v = value;
461      if (v.length() > 10) {
462        int i = value.substring(10).indexOf("-");
463        if (i == -1)
464          i = value.substring(10).indexOf("+");
465        v = i == -1 ? value : value.substring(0,  10+i);
466      }
467      if (v.length() > 10)
468        xst = "^^xsd:dateTime";
469      else if (v.length() == 10)
470        xst = "^^xsd:date";
471      else if (v.length() == 7)
472        xst = "^^xsd:gYearMonth";
473      else if (v.length() == 4)
474        xst = "^^xsd:gYear";
475    }
476          
477                return "\"" +Turtle.escape(value, true) + "\""+xst;
478        }
479
480  protected void decorateCoding(Complex t, Element coding, Section section) throws FHIRException {
481    String system = coding.getChildValue("system");
482    String code = coding.getChildValue("code");
483    
484    if (system == null)
485      return;
486    if ("http://snomed.info/sct".equals(system)) {
487      t.prefix("sct", "http://snomed.info/id/");
488      if (code.contains(":") || code.contains("="))
489        generateLinkedPredicate(t, code);
490      else
491        t.linkedPredicate("a", "sct:" + urlescape(code), null);
492    } else if ("http://loinc.org".equals(system)) {
493      t.prefix("loinc", "http://loinc.org/rdf#");
494      t.linkedPredicate("a", "loinc:"+urlescape(code).toUpperCase(), null);
495    }  
496  }
497  private void generateLinkedPredicate(Complex t, String code) throws FHIRException {
498    Expression expression = SnomedExpressions.parse(code);
499    
500  }
501
502
503//    128045006|cellulitis (disorder)|:{363698007|finding site|=56459004|foot structure|}
504//    Grahame Grieve: or
505//
506//    64572001|disease|:{116676008|associated morphology|=72704001|fracture|,363698007|finding site|=(12611008|bone structure of  tibia|:272741003|laterality|=7771000|left|)}
507//    Harold Solbrig:
508//    a sct:128045006,
509//      rdfs:subClassOf [
510//          a owl:Restriction;
511//          owl:onProperty sct:609096000 ;
512//          owl:someValuesFrom [
513//                a owl:Restriction;
514//                 owl:onProperty sct:363698007 ;
515//                owl:someValuesFrom sct:56459004 ] ] ;
516//    and
517//
518//    a sct:64572001,
519//       rdfs:subclassOf  [
520//           a owl:Restriction ;
521//           owl:onProperty sct:60909600 ;
522//           owl:someValuesFrom [ 
523//                 a owl:Class ;
524//                 owl:intersectionOf ( [
525//                      a owl:Restriction;
526//                      owl:onProperty sct:116676008;
527//                     owl:someValuesFrom sct:72704001 ] 
528//                 [  a owl:Restriction;
529//                      owl:onProperty sct:363698007 
530//                      owl:someValuesFrom [
531//                            a owl:Class ;
532//                            owl:intersectionOf(
533//                                 sct:12611008
534//                                 owl:someValuesFrom [
535//                                         a owl:Restriction;
536//                                         owl:onProperty sct:272741003;
537//                                         owl:someValuesFrom sct:7771000
538//                                  ] ) ] ] ) ] ]
539//    (an approximation -- I'll have to feed it into a translator to be sure I've got it 100% right)
540//
541  
542}