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