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