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