001package org.hl7.fhir.r4.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
032import java.io.IOException;
033import java.io.InputStream;
034import java.io.OutputStream;
035import java.util.HashSet;
036import java.util.List;
037import java.util.Set;
038
039import org.hl7.fhir.exceptions.FHIRException;
040import org.hl7.fhir.exceptions.FHIRFormatError;
041import org.hl7.fhir.r4.context.IWorkerContext;
042import org.hl7.fhir.r4.elementmodel.Element.SpecialElement;
043import org.hl7.fhir.r4.formats.IParser.OutputStyle;
044import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent;
045import org.hl7.fhir.r4.model.StructureDefinition;
046import org.hl7.fhir.r4.utils.SnomedExpressions;
047import org.hl7.fhir.r4.utils.SnomedExpressions.Expression;
048import org.hl7.fhir.r4.utils.formats.Turtle;
049import org.hl7.fhir.r4.utils.formats.Turtle.Complex;
050import org.hl7.fhir.r4.utils.formats.Turtle.Section;
051import org.hl7.fhir.r4.utils.formats.Turtle.Subject;
052import org.hl7.fhir.r4.utils.formats.Turtle.TTLComplex;
053import org.hl7.fhir.r4.utils.formats.Turtle.TTLList;
054import org.hl7.fhir.r4.utils.formats.Turtle.TTLLiteral;
055import org.hl7.fhir.r4.utils.formats.Turtle.TTLObject;
056import org.hl7.fhir.r4.utils.formats.Turtle.TTLURL;
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
062public class TurtleParser extends ParserBase {
063
064  private String base;
065
066  public static String FHIR_URI_BASE = "http://hl7.org/fhir/";
067  public static String FHIR_VERSION_BASE = "http://build.fhir.org/";
068
069  public TurtleParser(IWorkerContext context) {
070    super(context);
071  }
072
073  @Override
074  public Element parse(InputStream input) throws IOException, FHIRException {
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(),
081            IssueSeverity.FATAL);
082        return null;
083      }
084      return parse(src);
085    } else {
086      src.parse(TextFile.streamToString(input));
087      return parse(src);
088    }
089  }
090
091  private Element parse(Turtle src) throws FHIRException {
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
102        + "nodeRole = " + FHIR_URI_BASE + "treeRoot)";
103    if (policy == ValidationPolicy.EVERYTHING) {
104      logError(-1, -1, "(document)", IssueType.INVALID, msg, IssueSeverity.FATAL);
105      return null;
106    } else {
107      throw new FHIRFormatError(msg);
108    }
109  }
110
111  private Element parse(Turtle src, TTLComplex cmp) throws FHIRException {
112    TTLObject type = cmp.getPredicates().get("http://www.w3.org/2000/01/rdf-schema#type");
113    if (type == null) {
114      logError(cmp.getLine(), cmp.getCol(), "(document)", IssueType.INVALID,
115          "Unknown resource type (missing rdfs:type)", IssueSeverity.FATAL);
116      return null;
117    }
118    if (type instanceof TTLList) {
119      // this is actually broken - really we have to look through the structure
120      // definitions at this point
121      for (TTLObject obj : ((TTLList) type).getList()) {
122        if (obj instanceof TTLURL && ((TTLURL) obj).getUri().startsWith(FHIR_URI_BASE)) {
123          type = obj;
124          break;
125        }
126      }
127    }
128    if (!(type instanceof TTLURL)) {
129      logError(cmp.getLine(), cmp.getCol(), "(document)", IssueType.INVALID, "Unexpected datatype for rdfs:type)",
130          IssueSeverity.FATAL);
131      return null;
132    }
133    String name = ((TTLURL) type).getUri();
134    String ns = name.substring(0, name.lastIndexOf("/"));
135    name = name.substring(name.lastIndexOf("/") + 1);
136    String path = "/" + name;
137
138    StructureDefinition sd = getDefinition(cmp.getLine(), cmp.getCol(), ns, name);
139    if (sd == null)
140      return null;
141
142    Element result = new Element(name, new Property(context, sd.getSnapshot().getElement().get(0), sd));
143    result.markLocation(cmp.getLine(), cmp.getCol());
144    result.setType(name);
145    parseChildren(src, path, cmp, result, false);
146    result.numberChildren();
147    return result;
148  }
149
150  private void parseChildren(Turtle src, String path, TTLComplex object, Element context, boolean primitive)
151      throws FHIRException {
152
153    List<Property> properties = context.getProperty().getChildProperties(context.getName(), null);
154    Set<String> processed = new HashSet<String>();
155    if (primitive)
156      processed.add(FHIR_URI_BASE + "value");
157
158    // note that we do not trouble ourselves to maintain the wire format order here
159    // - we don't even know what it was anyway
160    // first pass: process the properties
161    for (Property property : properties) {
162      if (property.isChoice()) {
163        for (TypeRefComponent type : property.getDefinition().getType()) {
164          String eName = property.getName().substring(0, property.getName().length() - 3)
165              + Utilities.capitalize(type.getCode());
166          parseChild(src, object, context, processed, property, path, getFormalName(property, eName));
167        }
168      } else {
169        parseChild(src, object, context, processed, property, path, getFormalName(property));
170      }
171    }
172
173    // second pass: check for things not processed
174    if (policy != ValidationPolicy.NONE) {
175      for (String u : object.getPredicates().keySet()) {
176        if (!processed.contains(u)) {
177          TTLObject n = object.getPredicates().get(u);
178          logError(n.getLine(), n.getCol(), path, IssueType.STRUCTURE, "Unrecognised predicate '" + u + "'",
179              IssueSeverity.ERROR);
180        }
181      }
182    }
183  }
184
185  private void parseChild(Turtle src, TTLComplex object, Element context, Set<String> processed, Property property,
186      String path, String name) throws FHIRException {
187    processed.add(name);
188    String npath = path + "/" + property.getName();
189    TTLObject e = object.getPredicates().get(FHIR_URI_BASE + name);
190    if (e == null)
191      return;
192    if (property.isList() && (e instanceof TTLList)) {
193      TTLList arr = (TTLList) e;
194      for (TTLObject am : arr.getList()) {
195        parseChildInstance(src, npath, object, context, property, name, am);
196      }
197    } else {
198      parseChildInstance(src, npath, object, context, property, name, e);
199    }
200  }
201
202  private void parseChildInstance(Turtle src, String npath, TTLComplex object, Element context, Property property,
203      String name, TTLObject e) throws FHIRException {
204    if (property.isResource())
205      parseResource(src, npath, object, context, property, name, e);
206    else if (e instanceof TTLComplex) {
207      TTLComplex child = (TTLComplex) e;
208      Element n = new Element(tail(name), property).markLocation(e.getLine(), e.getCol());
209      context.getChildren().add(n);
210      if (property.isPrimitive(property.getType(tail(name)))) {
211        parseChildren(src, npath, child, n, true);
212        TTLObject val = child.getPredicates().get(FHIR_URI_BASE + "value");
213        if (val != null) {
214          if (val instanceof TTLLiteral) {
215            String value = ((TTLLiteral) val).getValue();
216            String type = ((TTLLiteral) val).getType();
217            // todo: check type
218            n.setValue(value);
219          } else
220            logError(object.getLine(), object.getCol(), npath, IssueType.INVALID,
221                "This property must be a Literal, not a " + e.getClass().getName(), IssueSeverity.ERROR);
222        }
223      } else
224        parseChildren(src, npath, child, n, false);
225
226    } else
227      logError(object.getLine(), object.getCol(), npath, IssueType.INVALID,
228          "This property must be a URI or bnode, not a " + e.getClass().getName(), IssueSeverity.ERROR);
229  }
230
231  private String tail(String name) {
232    return name.substring(name.lastIndexOf(".") + 1);
233  }
234
235  private void parseResource(Turtle src, String npath, TTLComplex object, Element context, Property property,
236      String name, TTLObject e) throws FHIRException {
237    TTLComplex obj;
238    if (e instanceof TTLComplex)
239      obj = (TTLComplex) e;
240    else if (e instanceof TTLURL) {
241      String url = ((TTLURL) e).getUri();
242      obj = src.getObject(url);
243      if (obj == null) {
244        logError(e.getLine(), e.getCol(), npath, IssueType.INVALID, "reference to " + url + " cannot be resolved",
245            IssueSeverity.FATAL);
246        return;
247      }
248    } else
249      throw new FHIRFormatError("Wrong type for resource");
250
251    TTLObject type = obj.getPredicates().get("http://www.w3.org/2000/01/rdf-schema#type");
252    if (type == null) {
253      logError(object.getLine(), object.getCol(), npath, IssueType.INVALID, "Unknown resource type (missing rdfs:type)",
254          IssueSeverity.FATAL);
255      return;
256    }
257    if (type instanceof TTLList) {
258      // this is actually broken - really we have to look through the structure
259      // definitions at this point
260      for (TTLObject tobj : ((TTLList) type).getList()) {
261        if (tobj instanceof TTLURL && ((TTLURL) tobj).getUri().startsWith(FHIR_URI_BASE)) {
262          type = tobj;
263          break;
264        }
265      }
266    }
267    if (!(type instanceof TTLURL)) {
268      logError(object.getLine(), object.getCol(), npath, IssueType.INVALID, "Unexpected datatype for rdfs:type)",
269          IssueSeverity.FATAL);
270      return;
271    }
272    String rt = ((TTLURL) type).getUri();
273    String ns = rt.substring(0, rt.lastIndexOf("/"));
274    rt = rt.substring(rt.lastIndexOf("/") + 1);
275
276    StructureDefinition sd = getDefinition(object.getLine(), object.getCol(), ns, rt);
277    if (sd == null)
278      return;
279
280    Element n = new Element(tail(name), property).markLocation(object.getLine(), object.getCol());
281    context.getChildren().add(n);
282    n.updateProperty(new Property(this.context, sd.getSnapshot().getElement().get(0), sd),
283        SpecialElement.fromProperty(n.getProperty()), property);
284    n.setType(rt);
285    parseChildren(src, npath, obj, n, false);
286  }
287
288  private String getFormalName(Property property) {
289    String en = property.getDefinition().getBase().getPath();
290    if (en == null)
291      en = property.getDefinition().getPath();
292//    boolean doType = false;
293//      if (en.endsWith("[x]")) {
294//        en = en.substring(0, en.length()-3);
295//        doType = true;        
296//      }
297//     if (doType || (element.getProperty().getDefinition().getType().size() > 1 && !allReference(element.getProperty().getDefinition().getType())))
298//       en = en + Utilities.capitalize(element.getType());
299    return en;
300  }
301
302  private String getFormalName(Property property, String elementName) {
303    String en = property.getDefinition().getBase().getPath();
304    if (en == null)
305      en = property.getDefinition().getPath();
306    if (!en.endsWith("[x]"))
307      throw new Error("Attempt to replace element name for a non-choice type");
308    return en.substring(0, en.lastIndexOf(".") + 1) + elementName;
309  }
310
311  @Override
312  public void compose(Element e, OutputStream stream, OutputStyle style, String base)
313      throws IOException, FHIRException {
314    this.base = base;
315
316    Turtle ttl = new Turtle();
317    compose(e, ttl, base);
318    ttl.commit(stream, false);
319  }
320
321  public void compose(Element e, Turtle ttl, String base) throws FHIRException {
322    ttl.prefix("fhir", FHIR_URI_BASE);
323    ttl.prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#");
324    ttl.prefix("owl", "http://www.w3.org/2002/07/owl#");
325    ttl.prefix("xsd", "http://www.w3.org/2001/XMLSchema#");
326
327    Section section = ttl.section("resource");
328    String subjId = genSubjectId(e);
329
330    String ontologyId = subjId.replace(">", ".ttl>");
331    Section ontology = ttl.section("ontology header");
332    ontology.triple(ontologyId, "a", "owl:Ontology");
333    ontology.triple(ontologyId, "owl:imports", "fhir:fhir.ttl");
334    if (ontologyId.startsWith("<" + FHIR_URI_BASE))
335      ontology.triple(ontologyId, "owl:versionIRI", ontologyId.replace(FHIR_URI_BASE, FHIR_VERSION_BASE));
336
337    Subject subject = section.triple(subjId, "a", "fhir:" + e.getType());
338    subject.linkedPredicate("fhir:nodeRole", "fhir:treeRoot",
339        linkResolver == null ? null : linkResolver.resolvePage("rdf.html#tree-root"));
340
341    for (Element child : e.getChildren()) {
342      composeElement(section, subject, child, null);
343    }
344
345  }
346
347  protected String getURIType(String uri) {
348    if (uri.startsWith("<" + FHIR_URI_BASE))
349      if (uri.substring(FHIR_URI_BASE.length() + 1).contains("/"))
350        return uri.substring(FHIR_URI_BASE.length() + 1, uri.indexOf('/', FHIR_URI_BASE.length() + 1));
351    return null;
352  }
353
354  protected String getReferenceURI(String ref) {
355    if (ref != null && (ref.startsWith("http://") || ref.startsWith("https://")))
356      return "<" + ref + ">";
357    else if (base != null && ref != null && ref.contains("/"))
358      return "<" + Utilities.appendForwardSlash(base) + ref + ">";
359    else
360      return null;
361  }
362
363  protected void decorateReference(Complex t, Element coding) {
364    String refURI = getReferenceURI(coding.getChildValue("reference"));
365    if (refURI != null)
366      t.linkedPredicate("fhir:link", refURI,
367          linkResolver == null ? null : linkResolver.resolvePage("rdf.html#reference"));
368  }
369
370  protected void decorateCanonical(Complex t, Element canonical) {
371    String refURI = getReferenceURI(canonical.primitiveValue());
372    if (refURI != null)
373      t.linkedPredicate("fhir:link", refURI,
374          linkResolver == null ? null : linkResolver.resolvePage("rdf.html#reference"));
375  }
376
377  private String genSubjectId(Element e) {
378    String id = e.getChildValue("id");
379    if (base == null || id == null)
380      return "";
381    else if (base.endsWith("#"))
382      return "<" + base + e.getType() + "-" + id + ">";
383    else
384      return "<" + Utilities.pathURL(base, e.getType(), id) + ">";
385  }
386
387  private String urlescape(String s) {
388    StringBuilder b = new StringBuilder();
389    for (char ch : s.toCharArray()) {
390      if (Utilities.charInSet(ch, ':', ';', '=', ','))
391        b.append("%" + Integer.toHexString(ch));
392      else
393        b.append(ch);
394    }
395    return b.toString();
396  }
397
398  private void composeElement(Section section, Complex ctxt, Element element, Element parent) throws FHIRException {
399//    "Extension".equals(element.getType())?
400//            (element.getProperty().getDefinition().getIsModifier()? "modifierExtension" : "extension") ; 
401    String en = getFormalName(element);
402
403    Complex t;
404    if (element.getSpecial() == SpecialElement.BUNDLE_ENTRY && parent != null
405        && parent.getNamedChildValue("fullUrl") != null) {
406      String url = "<" + parent.getNamedChildValue("fullUrl") + ">";
407      ctxt.linkedPredicate("fhir:" + en, url,
408          linkResolver == null ? null : linkResolver.resolveProperty(element.getProperty()));
409      t = section.subject(url);
410    } else {
411      t = ctxt.linkedPredicate("fhir:" + en,
412          linkResolver == null ? null : linkResolver.resolveProperty(element.getProperty()));
413    }
414    if (element.getSpecial() != null)
415      t.linkedPredicate("a", "fhir:" + element.fhirType(),
416          linkResolver == null ? null : linkResolver.resolveType(element.fhirType()));
417    if (element.hasValue())
418      t.linkedPredicate("fhir:value", ttlLiteral(element.getValue(), element.getType()),
419          linkResolver == null ? null : linkResolver.resolveType(element.getType()));
420    if (element.getProperty().isList() && (!element.isResource() || element.getSpecial() == SpecialElement.CONTAINED))
421      t.linkedPredicate("fhir:index", Integer.toString(element.getIndex()),
422          linkResolver == null ? null : linkResolver.resolvePage("rdf.html#index"));
423
424    if ("Coding".equals(element.getType()))
425      decorateCoding(t, element, section);
426    if (Utilities.existsInList(element.getType(), "Reference"))
427      decorateReference(t, element);
428    else if (Utilities.existsInList(element.getType(), "canonical"))
429      decorateCanonical(t, element);
430
431    if ("canonical".equals(element.getType())) {
432      String refURI = element.primitiveValue();
433      if (refURI != null) {
434        String uriType = getURIType(refURI);
435        if (uriType != null && !section.hasSubject(refURI))
436          section.triple(refURI, "a", "fhir:" + uriType);
437      }
438    }
439
440    if ("Reference".equals(element.getType())) {
441      String refURI = getReferenceURI(element.getChildValue("reference"));
442      if (refURI != null) {
443        String uriType = getURIType(refURI);
444        if (uriType != null && !section.hasSubject(refURI))
445          section.triple(refURI, "a", "fhir:" + uriType);
446      }
447    }
448
449    for (Element child : element.getChildren()) {
450      if ("xhtml".equals(child.getType())) {
451        String childfn = getFormalName(child);
452        t.predicate("fhir:" + childfn, ttlLiteral(child.getValue(), child.getType()));
453      } else
454        composeElement(section, t, child, element);
455    }
456  }
457
458  private String getFormalName(Element element) {
459    String en = null;
460    if (element.getSpecial() == null) {
461      if (element.getProperty().getDefinition().hasBase())
462        en = element.getProperty().getDefinition().getBase().getPath();
463    } else if (element.getSpecial() == SpecialElement.BUNDLE_ENTRY)
464      en = "Bundle.entry.resource";
465    else if (element.getSpecial() == SpecialElement.BUNDLE_OUTCOME)
466      en = "Bundle.entry.response.outcome";
467    else if (element.getSpecial() == SpecialElement.PARAMETER)
468      en = element.getElementProperty().getDefinition().getPath();
469    else // CONTAINED
470      en = "DomainResource.contained";
471
472    if (en == null)
473      en = element.getProperty().getDefinition().getPath();
474    boolean doType = false;
475    if (en.endsWith("[x]")) {
476      en = en.substring(0, en.length() - 3);
477      doType = true;
478    }
479    if (doType || (element.getProperty().getDefinition().getType().size() > 1
480        && !allReference(element.getProperty().getDefinition().getType())))
481      en = en + Utilities.capitalize(element.getType());
482    return en;
483  }
484
485  private boolean allReference(List<TypeRefComponent> types) {
486    for (TypeRefComponent t : types) {
487      if (!t.getCode().equals("Reference"))
488        return false;
489    }
490    return true;
491  }
492
493  static public String ttlLiteral(String value, String type) {
494    String xst = "";
495    if (type.equals("boolean"))
496      xst = "^^xsd:boolean";
497    else if (type.equals("integer"))
498      xst = "^^xsd:integer";
499    else if (type.equals("unsignedInt"))
500      xst = "^^xsd:nonNegativeInteger";
501    else if (type.equals("positiveInt"))
502      xst = "^^xsd:positiveInteger";
503    else if (type.equals("decimal"))
504      xst = "^^xsd:decimal";
505    else if (type.equals("base64Binary"))
506      xst = "^^xsd:base64Binary";
507    else if (type.equals("instant"))
508      xst = "^^xsd:dateTime";
509    else if (type.equals("time"))
510      xst = "^^xsd:time";
511    else if (type.equals("date") || type.equals("dateTime")) {
512      String v = value;
513      if (v.length() > 10) {
514        int i = value.substring(10).indexOf("-");
515        if (i == -1)
516          i = value.substring(10).indexOf("+");
517        v = i == -1 ? value : value.substring(0, 10 + i);
518      }
519      if (v.length() > 10)
520        xst = "^^xsd:dateTime";
521      else if (v.length() == 10)
522        xst = "^^xsd:date";
523      else if (v.length() == 7)
524        xst = "^^xsd:gYearMonth";
525      else if (v.length() == 4)
526        xst = "^^xsd:gYear";
527    }
528
529    return "\"" + Turtle.escape(value, true) + "\"" + xst;
530  }
531
532  protected void decorateCoding(Complex t, Element coding, Section section) throws FHIRException {
533    String system = coding.getChildValue("system");
534    String code = coding.getChildValue("code");
535
536    if (system == null || code == null)
537      return;
538    if ("http://snomed.info/sct".equals(system)) {
539      t.prefix("sct", "http://snomed.info/id/");
540      if (code.contains(":") || code.contains("="))
541        generateLinkedPredicate(t, code);
542      else
543        t.linkedPredicate("a", "sct:" + urlescape(code), null);
544    } else if ("http://loinc.org".equals(system)) {
545      t.prefix("loinc", "http://loinc.org/rdf#");
546      t.linkedPredicate("a", "loinc:" + urlescape(code).toUpperCase(), null);
547    }
548  }
549
550  private void generateLinkedPredicate(Complex t, String code) throws FHIRException {
551    Expression expression = SnomedExpressions.parse(code);
552
553  }
554
555//    128045006|cellulitis (disorder)|:{363698007|finding site|=56459004|foot structure|}
556//    Grahame Grieve: or
557//
558//    64572001|disease|:{116676008|associated morphology|=72704001|fracture|,363698007|finding site|=(12611008|bone structure of  tibia|:272741003|laterality|=7771000|left|)}
559//    Harold Solbrig:
560//    a sct:128045006,
561//      rdfs:subClassOf [
562//          a owl:Restriction;
563//          owl:onProperty sct:609096000 ;
564//          owl:someValuesFrom [
565//                a owl:Restriction;
566//                 owl:onProperty sct:363698007 ;
567//                owl:someValuesFrom sct:56459004 ] ] ;
568//    and
569//
570//    a sct:64572001,
571//       rdfs:subclassOf  [
572//           a owl:Restriction ;
573//           owl:onProperty sct:60909600 ;
574//           owl:someValuesFrom [ 
575//                 a owl:Class ;
576//                 owl:intersectionOf ( [
577//                      a owl:Restriction;
578//                      owl:onProperty sct:116676008;
579//                     owl:someValuesFrom sct:72704001 ] 
580//                 [  a owl:Restriction;
581//                      owl:onProperty sct:363698007 
582//                      owl:someValuesFrom [
583//                            a owl:Class ;
584//                            owl:intersectionOf(
585//                                 sct:12611008
586//                                 owl:someValuesFrom [
587//                                         a owl:Restriction;
588//                                         owl:onProperty sct:272741003;
589//                                         owl:someValuesFrom sct:7771000
590//                                  ] ) ] ] ) ] ]
591//    (an approximation -- I'll have to feed it into a translator to be sure I've got it 100% right)
592//
593
594}