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}