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