001package org.hl7.fhir.dstu2016may.utils;
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.util.ArrayList;
035import java.util.Collections;
036import java.util.Comparator;
037import java.util.HashSet;
038import java.util.LinkedList;
039import java.util.List;
040import java.util.Set;
041
042import org.apache.commons.lang3.StringUtils;
043import org.apache.commons.lang3.tuple.ImmutablePair;
044import org.apache.commons.lang3.tuple.Pair;
045import org.hl7.fhir.dstu2016may.model.ElementDefinition;
046import org.hl7.fhir.dstu2016may.model.StructureDefinition;
047import org.stringtemplate.v4.ST;
048
049public class ShExGenerator {
050
051  public enum HTMLLinkPolicy {
052    NONE, EXTERNAL, INTERNAL
053  }
054
055  // An entire definition is a header plus a list of shape definitions
056  private static String SHEX_TEMPLATE =
057          "$header$\n\n" +
058          "$shapeDefinitions$";
059
060  // A header is a list of prefixes plus a BASE
061  private static String HEADER_TEMPLATE =
062          "PREFIX fhir: <http://hl7.org/fhir/> \n" +
063          "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> \n" +
064          "BASE <http://hl7.org/fhir/shape/>\n";
065
066  // A shape definition template -- an id followed by an optional resource declaration (type + treeRoot) +
067  // a list of definition elements
068  private static String SHAPE_DEFINITION_TEMPLATE = "<$id$> {\n$resourceDecl$\t$elements$\n}\n";
069
070  // Additional declaration that appears only in a resource definition
071  private static String RESOURCE_DECL_TEMPLATE = "\n\ta [fhir:$id$],\n\tfhir:nodeRole [fhir:treeRoot],\n";
072
073  // An element definition within a shape
074  private static String ELEMENT_TEMPLATE = "fhir:$id$ $defn$$card$";
075
076  // A simple element definition
077  private static String SIMPLE_ELEMENT_TEMPLATE = "@<$typ$>";
078
079  // A primitive element definition
080  private static String PRIMITIVE_ELEMENT_TEMPLATE = "xsd:$typ$";
081
082  // A list of alternative shape definitions
083  private static String ALTERNATIVE_TEMPLATE = "\n\t(\t$altEntries$\n\t)";
084
085  // A typed reference definition
086  private static String REFERENCE_TEMPLATE = "@<$ref$Reference>";
087
088  // A choice of different predicate / types
089  private static String CHOICE_TEMPLATE = "\n(\t$choiceEntries$\n\t)$card$";
090
091  // A typed reference -- a fhir:uri with an optional type and the possibility of a resolvable shape
092  private static String TYPED_REFERENCE_TEMPLATE = "\n<$refType$Reference> {\n" +
093                                                   "\ta [fhir:$refType$Reference]?,\n" +
094                                                   "\tfhir:uri.id @<id>?,\n" +
095                                                   "\tfhir:uri.extension @<Extension>*,\n" +
096                                                   "\tfhir:uri.value (xsd:anyURI OR @<$refType$>)\n" +
097                                                   "}";
098
099  // TODO: find the literal for this
100  private static String XML_DEFN_TYPE = "http://hl7.org/fhir/StructureDefinition/structuredefinition-xml-type";
101
102  /**
103   * this makes internal metadata services available to the generator - retrieving structure definitions, and value set expansion etc
104   */
105  private IWorkerContext context;
106
107  /**
108   * List of typeReferences generated per session
109   */
110    private LinkedList<Pair<StructureDefinition, ElementDefinition>> typeReferences;
111    private HashSet<String> references;
112
113  public ShExGenerator(IWorkerContext context) {
114    super();
115    this.context = context;
116    this.typeReferences = new LinkedList<Pair<StructureDefinition, ElementDefinition>>();
117    this.references = new HashSet<String>();
118  }
119  
120  public String generate(HTMLLinkPolicy links, StructureDefinition structure) {
121    List<StructureDefinition> list = new ArrayList<StructureDefinition>();
122    list.add(structure);
123    this.typeReferences.clear();
124    this.references.clear();
125    return generate(links, list);
126  }
127  
128  public class SortById implements Comparator<StructureDefinition> {
129
130    @Override
131    public int compare(StructureDefinition arg0, StructureDefinition arg1) {
132      return arg0.getId().compareTo(arg1.getId());
133    }
134
135  }
136
137  private ST tmplt(String template) {
138    return new ST(template, '$', '$');
139  }
140
141  /** 
142   * this is called externally to generate a set of structures to a single ShEx file
143   * generally, it will be called with a single structure, or a long list of structures (all of them)
144   * 
145   * @param links HTML link rendering policy
146   * @param structures list of structure definitions to render
147   * @return ShEx definition of structures
148   */
149  public String generate(HTMLLinkPolicy links, List<StructureDefinition> structures) {
150    ST shex_def = tmplt(SHEX_TEMPLATE);
151    shex_def.add("header", genHeader());
152    
153    // Process the requested definitions
154    Collections.sort(structures, new SortById());
155    StringBuilder shapeDefinitions = new StringBuilder();
156    for (StructureDefinition sd : structures) {
157      shapeDefinitions.append(genShapeDefinition(sd, true));
158    }
159
160    // Add the inner types
161    Set<String> seen = new HashSet<String>();
162    while(!this.typeReferences.isEmpty()) {
163      Pair<StructureDefinition, ElementDefinition> sded = this.typeReferences.poll();
164      StructureDefinition sd = sded.getLeft();
165      ElementDefinition ed = sded.getRight();
166      if (seen.contains(ed.getPath()))
167        break;
168      seen.add(ed.getPath());
169      shapeDefinitions.append("\n" + genElementReference(sd, ed));
170    }
171
172    // Add the specific reference types
173    for(String r: this.references) {
174      shapeDefinitions.append("\n" + genReferenceEntry(r) + "\n");
175    }
176      
177    shex_def.add("shapeDefinitions", shapeDefinitions);
178    return shex_def.render();
179      }
180      
181  /**
182   * Generate the ShEx Header
183   * @return String representation
184   */
185  private String genHeader() {
186    return HEADER_TEMPLATE;
187    }
188    
189  /**
190   * Emit a ShEx definition for the supplied StructureDefinition
191   * @param sd Structure definition to emit
192   * @return ShEx definition
193   */
194  private String genShapeDefinition(StructureDefinition sd, boolean isResource) {
195    ST resource_decl = tmplt(RESOURCE_DECL_TEMPLATE);
196    ST struct_def = tmplt(SHAPE_DEFINITION_TEMPLATE);
197
198    // todo: Figure out why this doesn't identify "Account" as a resource.  getResourceNames() returns "Resource", "Resource"
199    // this.context.getResourceNames().contains(sd.getId())
200    resource_decl.add("id", sd.getId());
201    struct_def.add("resourceDecl", isResource ? resource_decl.render() : "");
202
203    struct_def.add("id", sd.getId());
204    List<String> elements = new ArrayList<String>();
205
206    for (ElementDefinition ed : sd.getSnapshot().getElement()) {
207      if(StringUtils.countMatches(ed.getPath(), ".") == 1) {
208        elements.add(genElementDefinition(sd, ed));
209      }
210    }
211    struct_def.add("elements", StringUtils.join(elements, ",\n\t"));
212    return struct_def.render();
213  }
214
215  /**
216   * Generate a ShEx element definition
217   * @param sd Containing structure definition
218   * @param ed Containing element definition
219   * @return ShEx definition
220   */
221  private String genElementDefinition(StructureDefinition sd, ElementDefinition ed) {
222    ST element_def =  tmplt(ELEMENT_TEMPLATE);
223
224    String id = ed.hasBase() ? ed.getBase().getPath() : ed.getPath();
225    String card = "*".equals(ed.getMax()) ? (ed.getMin() == 0 ? "*" : "+") : "?";
226    String defn;
227    element_def.add("id", id);
228    element_def.add("card", card);
229    
230    List<ElementDefinition> children = ProfileUtilities.getChildList(sd, ed);
231    if (children.size() > 0) {
232      // inline anonymous type - give it a name and factor it out
233      this.typeReferences.add(new ImmutablePair<StructureDefinition, ElementDefinition>(sd, ed));
234      ST anon_link = tmplt(SIMPLE_ELEMENT_TEMPLATE);
235      anon_link.add("typ", id);
236      defn = anon_link.render();
237    } else if (ed.getType().size() == 1) {
238      // Single entry
239      defn = genTypeRef(ed.getType().get(0));
240    } else { 
241      // multiple types
242      // todo: figure out how to do this with the API
243      if(id.endsWith("[x]")) {
244        return genChoiceTypes(ed, id, card);
245      } else {
246        defn = genAlternativeTypes(ed, id, card);
247      }
248      }
249    element_def.add("defn", defn);
250    return element_def.render();
251      }
252
253  private String genTypeRef(ElementDefinition.TypeRefComponent typ) {
254    ST single_entry = tmplt(SIMPLE_ELEMENT_TEMPLATE);
255    if(typ.getProfile().size() > 0) {
256      if (typ.getProfile().size() != 1) throw new AssertionError("Can't handle multiple profiles");
257      single_entry.add("typ", getProfiledType(typ));
258      // TODO: Figure out how to get into the interior codes
259//    } else if (typ.code.hasExtension()) {
260//        for (Extension ext : typ.getExtension()) {
261//          if(ext.getUrl() == XML_DEFN_TYPE ) {
262//            ST primitive_entry = tmplt(PRIMITIVE_ELEMENT_TEMPLATE);
263//            primitive_entry.add("typ", ext.getValue());
264//            return primitive_entry.render();
265//          }
266//        }
267//        return "UNKNOWN";
268      } else if (typ.getCode() == null) {
269        ST primitive_entry = tmplt(PRIMITIVE_ELEMENT_TEMPLATE);
270        primitive_entry.add("typ", "string");
271        return primitive_entry.render();
272      } else
273        single_entry.add("typ", typ.getCode());
274    return single_entry.render();
275  }
276
277        /**
278   * Generate a set of alternative shapes
279     * @param ed Containing element definition
280     * @param id Element definition identifier
281     * @param card Cardinality
282     * @return ShEx list of alternative anonymous shapes separated by "OR"
283         */
284  private String genAlternativeTypes(ElementDefinition ed, String id, String card) {
285    ST shex_alt = tmplt(ALTERNATIVE_TEMPLATE);
286    List<String> altEntries = new ArrayList<String>();
287
288    shex_alt.add("id", id);
289
290    for(ElementDefinition.TypeRefComponent typ : ed.getType())  {
291      altEntries.add(genAltEntry(id, typ));
292    }
293    shex_alt.add("altEntries", StringUtils.join(altEntries, " OR\n\t\t"));
294    shex_alt.add("card", card);
295    return shex_alt.render();
296  }
297
298  private String genReference(String id, ElementDefinition.TypeRefComponent  typ) {
299    ST shex_ref = tmplt(REFERENCE_TEMPLATE);
300
301    // todo: There has to be a better way to do this
302    String ref;
303    if(typ.getProfile().size() > 0) {
304      String[] els = typ.getProfile().get(0).getValue().split("/");
305      ref = els[els.length - 1];
306    } else { 
307      ref = "";
308    }
309    shex_ref.add("id", id);
310    shex_ref.add("ref", ref);
311    this.references.add(ref);
312    return shex_ref.render();
313  }
314
315  /**
316   * Generate an alternative shape for a reference
317   * @param id reference name
318   * @param typ shape type
319   * @return ShEx equivalent
320   */
321  private String genAltEntry(String id, ElementDefinition.TypeRefComponent typ) {
322    if(!typ.getCode().equals("Reference"))
323      throw new AssertionError("We do not handle " + typ.getCode() + " alternatives");
324
325    return genReference(id, typ);
326  }
327
328  /**
329   * Return the type definition for a profiled type
330   * @param typ type to generate the entry for
331   * @return string of type
332   */
333  private String getProfiledType(ElementDefinition.TypeRefComponent typ)
334  {
335    return typ.getProfile().get(0).fhirType();
336  }
337
338  /**
339   * Generate a list of type choices for a "name[x]" style id
340   * @param ed containing elmentdefinition
341   * @param id choice identifier
342   * @return ShEx fragment for the set of choices
343   */
344  private String genChoiceTypes(ElementDefinition ed, String id, String card) {
345    ST shex_choice = tmplt(CHOICE_TEMPLATE);
346    List<String> choiceEntries = new ArrayList<String>();
347    String base = id.replace("[x]", "");
348
349    for(ElementDefinition.TypeRefComponent typ : ed.getType())  {
350      choiceEntries.add(genChoiceEntry(base, typ));
351    }
352    shex_choice.add("choiceEntries", StringUtils.join(choiceEntries, " |\n\t\t"));
353    shex_choice.add("card", card);
354    return shex_choice.render();
355    }
356
357  /**
358   * Generate an entry in a choice list
359   * @param base base identifier
360   * @param typ type/discriminant
361   * @return ShEx fragment for choice entry
362   */
363  private String genChoiceEntry(String base, ElementDefinition.TypeRefComponent typ) {
364    ST shex_choice_entry = tmplt(ELEMENT_TEMPLATE);
365
366    String ext = typ.getCode();
367    shex_choice_entry.add("id", base+ext);
368    shex_choice_entry.add("card", "");
369    shex_choice_entry.add("defn", genTypeRef(typ));
370    return shex_choice_entry.render();
371  }
372
373  /**
374   * Generate a definition for a referenced element
375   * @param sd StructureDefinition in which the element was referenced
376   * @param ed Contained element definition
377   * @return ShEx representation of element reference
378   */
379  private String genElementReference(StructureDefinition sd, ElementDefinition ed) {
380    ST element_reference = tmplt(SHAPE_DEFINITION_TEMPLATE);
381    element_reference.add("resourceDecl", "");  // Not a resource
382    element_reference.add("id", ed.getPath());
383    List<String> elements = new ArrayList<String>();
384
385    for (ElementDefinition child: ProfileUtilities.getChildList(sd, ed)) {
386      elements.add(genElementDefinition(sd, child));
387    }
388
389    element_reference.add("elements", StringUtils.join(elements, ",\n\t"));
390    return element_reference.render();
391  }
392
393  /**
394   * Generate a reference to a typed fhir:uri
395   * @param refType reference type name
396   * @return reference
397   */
398  private String genReferenceEntry(String refType) {
399    ST typed_ref = tmplt(TYPED_REFERENCE_TEMPLATE);
400    typed_ref.add("refType", refType);
401    return typed_ref.render();
402  }
403
404
405}