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}