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}