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