001package org.hl7.fhir.r5.utils;
002
003import java.io.IOException;
004import java.util.Date;
005import java.util.HashMap;
006import java.util.Map;
007
008import org.hl7.fhir.exceptions.FHIRException;
009import org.hl7.fhir.r5.context.IWorkerContext;
010import org.hl7.fhir.r5.model.ElementDefinition;
011import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
012import org.hl7.fhir.r5.model.Enumerations.FHIRVersion;
013import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
014import org.hl7.fhir.r5.model.StructureDefinition;
015import org.hl7.fhir.r5.model.StructureDefinition.ExtensionContextType;
016import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
017import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule;
018import org.hl7.fhir.r5.model.UriType;
019import org.hl7.fhir.utilities.Utilities;
020import org.hl7.fhir.utilities.VersionUtilities;
021import org.hl7.fhir.utilities.json.model.JsonElement;
022import org.hl7.fhir.utilities.json.model.JsonObject;
023import org.hl7.fhir.utilities.json.parser.JsonParser;
024import org.hl7.fhir.utilities.npm.PackageHacker;
025
026public class XVerExtensionManager {
027
028  public enum XVerExtensionStatus {
029    BadVersion, Unknown, Invalid, Valid
030  }
031
032  public static final String XVER_EXT_MARKER = "XVER_EXT_MARKER";
033
034  private Map<String, JsonObject> lists = new HashMap<>();
035  private IWorkerContext context;
036
037  public XVerExtensionManager(IWorkerContext context) {
038    this.context = context;
039  }
040
041  public boolean isR5(String url) {
042    String v = url.substring(20, 23);
043    return "5.0".equals(v);    
044  }
045  
046  public XVerExtensionStatus status(String url) throws FHIRException {
047    if (url.length() < 24) {
048      return XVerExtensionStatus.Invalid;
049    }
050    String v = url.substring(20, 23);
051    String e = url.substring(54);
052    if (!lists.containsKey(v)) {
053      if (context.hasBinaryKey("xver-paths-"+v+".json")) {
054        try {
055          lists.put(v, JsonParser.parseObject(context.getBinaryForKey("xver-paths-"+v+".json")));
056        } catch (IOException e1) {
057          throw new FHIRException(e);
058        }
059      } else {
060        return XVerExtensionStatus.BadVersion;
061      }
062    }
063    JsonObject root = lists.get(v);
064    JsonObject path = root.getJsonObject(e);
065    if (path == null) {
066      path = root.getJsonObject(e+"[x]");      
067    }
068    if (path == null) {
069      return XVerExtensionStatus.Unknown;
070    }
071    if (path.has("elements") || path.has("types")) {
072      return XVerExtensionStatus.Valid;
073    } else {
074      return XVerExtensionStatus.Invalid;
075    }
076  }
077
078  public String getElementId(String url) {
079    return url.substring(54);
080  }
081
082  public StructureDefinition makeDefinition(String url) {
083    String verSource = url.substring(20, 23);
084    String verTarget = VersionUtilities.getMajMin(context.getVersion());
085    String e = url.substring(54);
086    String r = e.contains(".") ? e.substring(0, e.indexOf(".")) : e;
087    JsonObject root = lists.get(verSource);
088    JsonObject path = root.getJsonObject(e);
089    if (path == null) {
090      path = root.getJsonObject(e+"[x]");
091    }
092    
093    StructureDefinition sd = new StructureDefinition();
094    sd.setUserData(XVER_EXT_MARKER, "true");
095    if (context.getResourceNamesAsSet().contains(r)) {
096      sd.setWebPath(Utilities.pathURL(context.getSpecUrl(), r.toLowerCase()+"-definitions.html#"+e));
097    } else {
098      sd.setWebPath(PackageHacker.fixPackageUrl("https://hl7.org/fhir/versions.html#extensions"));
099    }
100    sd.setUrl(url);
101    sd.setVersion(context.getVersion());
102    sd.setFhirVersion(FHIRVersion.fromCode(context.getVersion()));
103    sd.setKind(StructureDefinitionKind.COMPLEXTYPE);
104    sd.setType("Extension");
105    sd.setDerivation(TypeDerivationRule.CONSTRAINT);
106    sd.setName("Extension-"+verSource+"-"+e);
107    sd.setTitle("Extension Definition for "+e+" for Version "+verSource);
108    sd.setStatus(PublicationStatus.ACTIVE);
109    sd.setExperimental(false);
110    sd.setDate(new Date());
111    sd.setPublisher("FHIR Project");
112    sd.setPurpose("Defined so the validator can validate cross version extensions (see http://hl7.org/fhir/versions.html#extensions)");
113    sd.setAbstract(false);
114    sd.addContext().setType(ExtensionContextType.ELEMENT).setExpression(head(e));
115    sd.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/Extension");
116    if (path.has("types")) {
117      sd.getDifferential().addElement().setPath("Extension.extension").setMax("0");
118      sd.getDifferential().addElement().setPath("Extension.url").setFixed(new UriType(url));
119      ElementDefinition val = sd.getDifferential().addElement().setPath("Extension.value[x]").setMin(1);
120      populateTypes(path, val, verSource, verTarget);
121    } else if (path.has("elements")) {
122      for (JsonElement i : path.forceArray("elements").getItems()) {
123        String apath = e+"."+i.asString();
124        JsonObject elt = root.getJsonObject(apath);
125        if (elt != null) {
126          genExtensionContents(root, apath, verSource, verTarget, sd, i, elt, "Extension.extension");
127        }
128      }      
129      sd.getDifferential().addElement().setPath("Extension.url").setFixed(new UriType(url));
130      sd.getDifferential().addElement().setPath("Extension.value[x]").setMax("0");
131    } else {
132      throw new FHIRException("Internal error - attempt to define extension for "+url+" when it is invalid");
133    }
134    if (path.has("modifier") && path.asBoolean("modifier")) {
135      ElementDefinition baseDef = new ElementDefinition("Extension");
136      sd.getDifferential().getElement().add(0, baseDef);
137      baseDef.setIsModifier(true);
138    }
139    return sd;
140  }
141
142  private void genExtensionContents(JsonObject root, String apath, String verSource, String verTarget, StructureDefinition sd, JsonElement i, JsonObject elt, String epath) {
143    String s = i.asString().replace("[x]", "");
144    sd.getDifferential().addElement().setPath(epath).setSliceName(s);
145    if (elt.has("types")) {            
146      sd.getDifferential().addElement().setPath(epath+".extension").setMax("0");
147      sd.getDifferential().addElement().setPath(epath+".url").setFixed(new UriType(s));
148      ElementDefinition val = sd.getDifferential().addElement().setPath(epath+".value[x]").setMin(1);
149      populateTypes(elt, val, verSource, verTarget);
150    } else if (elt.has("elements")) {
151      for (JsonElement ic : elt.forceArray("elements").getItems()) { 
152        String apathC = apath+"."+ic.asString();
153        JsonObject eltC = root.getJsonObject(apathC);
154        if (eltC != null) {
155          genExtensionContents(root, apathC, verSource, verTarget, sd, ic, eltC, epath+".extension");
156        }
157      }
158      sd.getDifferential().addElement().setPath(epath+".url").setFixed(new UriType(s));
159      sd.getDifferential().addElement().setPath(epath+".value[x]").setMax("0");
160    } else {
161      throw new FHIRException("Internal error - unknown element "+apath);
162    }
163  }
164
165  public void populateTypes(JsonObject path, ElementDefinition val, String verSource, String verTarget) {
166    for (JsonElement i : path.forceArray("types").getItems()) {
167      String s = i.asString();
168      if (!s.startsWith("!")) {
169        if (s.contains("(")) {
170          String t = s.substring(0, s.indexOf("("));
171          TypeRefComponent tr = val.addType().setCode(translateDataType(verTarget, t));
172          if (hasTargets(tr.getCode()) ) {
173            s = s.substring(t.length()+1);
174            for (String p : s.substring(0, s.length()-1).split("\\|")) {
175              if ("Any".equals(p)) {
176                tr.addTargetProfile("http://hl7.org/fhir/StructureDefinition/Resource");
177              } else if (p.contains(",")) {
178                for (String pp : p.split("\\,")) {
179                  if (isResource(pp)) {
180                    tr.addTargetProfile("http://hl7.org/fhir/StructureDefinition/"+pp);
181                  }
182                }
183              } else if (isResource(p)) {
184                tr.addTargetProfile("http://hl7.org/fhir/StructureDefinition/"+p);              
185              }
186            }
187          }
188        } else {
189          val.addType().setCode(translateDataType(verTarget, s));
190        }
191      }
192    }
193  }
194
195  // todo: translate names 
196  
197  private boolean isResource(String p) {
198    return context.getResourceNames().contains(p);
199  }
200
201  private boolean hasTargets(String dt) {
202    return Utilities.existsInList(dt, "canonical", "Reference", "CodeableReference");
203  }
204
205  private String translateDataType(String v, String dt) {
206    if (VersionUtilities.versionsCompatible("1.0", v) || VersionUtilities.versionsCompatible("1.4", v)) {
207      return translateToR2(dt);
208    } else if (VersionUtilities.versionsCompatible("3.0", v)) {
209      return translateToR3(dt);
210    } else {
211      return dt;
212    }
213  }
214
215  private String translateToR3(String dt) {
216    if ("canonical".equals(dt)) {
217      return "uri";
218    } else if ("url".equals(dt)) {
219      return "uri";
220    } else {
221      return dt;
222    }
223  }
224
225  private String translateToR2(String dt) {
226    if ("canonical".equals(dt)) {
227      return "uri";
228    } else if ("url".equals(dt)) {
229      return "uri";
230    } else if ("uuid".equals(dt)) {
231      return "id";
232    } else {
233      return dt;
234    }
235  }
236
237  private String head(String id) {
238    if (id.contains(".")) {
239      return id.substring(0, id.lastIndexOf("."));
240    } else {
241      return id;
242    }
243  }
244  
245  public String getVersion(String url) {
246    return url.substring(20, 23);
247  }
248
249  public boolean matchingUrl(String url) {
250    if (url == null || url.length() < 56) {
251      return false;
252    }
253    String pfx = url.substring(0, 20);
254    String v = url.substring(20, 23);
255    String sfx = url.substring(23, 54);
256    return pfx.equals("http://hl7.org/fhir/") &&
257       isVersionPattern(v) && sfx.equals("/StructureDefinition/extension-");
258  }
259
260  private boolean isVersionPattern(String v) {
261    return v.length() == 3 && Character.isDigit(v.charAt(0)) && v.charAt(1) == '.' && Character.isDigit(v.charAt(2));
262  }
263
264  public String getReference(String url) {
265    String version = getVersion(url);
266    String base = VersionUtilities.getSpecUrl(version);
267    if (base == null) {
268      return null;
269    } else {
270      String path = url.substring(url.indexOf("-")+1);
271      if (!path.contains(".")) {
272        return null;
273      }
274      String type = path.substring(0, path.indexOf("."));
275      if (Utilities.existsInList(type, "Annotation", "Attachment", "Identifier", "CodeableConcept", "Coding", "Quantity", "Duration", "Range", "Period", "Ratio", "RatioRange", "SampledData", "Signature", "HumanName", "Address", "ContactPoint", "Timing")) {
276        return Utilities.pathURL(base, "datatypes-definitions.html#"+path+"|"+VersionUtilities.getNameForVersion(version)+" "+path);
277      }
278      if (Utilities.existsInList(type, "Element", "BackboneElement", "BackboneType", "PrimitiveType", "DataType", "Base")) {
279        return Utilities.pathURL(base, "types-definitions.html#"+path+"|"+VersionUtilities.getNameForVersion(version)+" "+path);
280      }
281      if (Utilities.existsInList(type, "UsageContext", "RelatedArtifact", "DataRequirement", "ParameterDefinition", "TriggerDefinition", "Expression", "ContactDetail", "ExtendedContactDetail", "VirtualServiceDetail", "Availability", "MonetaryComponent", "Contributor")) {
282        return Utilities.pathURL(base, "metadatatypes-definitions.html#"+path+"|"+VersionUtilities.getNameForVersion(version)+" "+path);
283      }
284      if (Utilities.existsInList(type, "Reference", "CodeableReference")) {
285        return Utilities.pathURL(base, "references-definitions.html#"+path+"|"+VersionUtilities.getNameForVersion(version)+" "+path);
286      }
287      if (Utilities.existsInList(type, "Meta")) {
288        return Utilities.pathURL(base, "resource-definitions.html#"+path+"|"+VersionUtilities.getNameForVersion(version)+" "+path);
289      }
290      return Utilities.pathURL(base, type.toLowerCase()+"-definitions.html#"+path+"|"+VersionUtilities.getNameForVersion(version)+" "+path);
291    }
292  }
293
294}