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.Constants;
011import org.hl7.fhir.r5.model.ElementDefinition;
012import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
013import org.hl7.fhir.r5.model.Enumerations.FHIRVersion;
014import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
015import org.hl7.fhir.r5.model.StructureDefinition;
016import org.hl7.fhir.r5.model.StructureDefinition.ExtensionContextType;
017import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
018import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule;
019import org.hl7.fhir.r5.model.UriType;
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
027public class XVerExtensionManager {
028
029  public enum XVerExtensionStatus {
030    BadVersion, Unknown, Invalid, Valid
031  }
032
033  public static final String XVER_EXT_MARKER = "XVER_EXT_MARKER";
034
035  private Map<String, JsonObject> lists = new HashMap<>();
036  private IWorkerContext context;
037
038  public XVerExtensionManager(IWorkerContext context) {
039    this.context = context;
040  }
041
042  public boolean isR5(String url) {
043    String v = url.substring(20, 23);
044    return "5.0".equals(v);    
045  }
046  
047  public XVerExtensionStatus status(String url) throws FHIRException {
048    if (url.length() < 24) {
049      return XVerExtensionStatus.Invalid;
050    }
051    String v = url.substring(20, 23);
052    String e = url.substring(54);
053    if (!lists.containsKey(v)) {
054      if (context.hasBinaryKey("xver-paths-"+v+".json")) {
055        try {
056          lists.put(v, JsonParser.parseObject(context.getBinaryForKey("xver-paths-"+v+".json")));
057        } catch (IOException e1) {
058          throw new FHIRException(e);
059        }
060      } else {
061        return XVerExtensionStatus.BadVersion;
062      }
063    }
064    JsonObject root = lists.get(v);
065    JsonObject path = root.getJsonObject(e);
066    if (path == null) {
067      path = root.getJsonObject(e+"[x]");      
068    }
069    if (path == null) {
070      return XVerExtensionStatus.Unknown;
071    }
072    if (path.has("elements") || path.has("types")) {
073      return XVerExtensionStatus.Valid;
074    } else {
075      return XVerExtensionStatus.Invalid;
076    }
077  }
078
079  public String getElementId(String url) {
080    return url.substring(54);
081  }
082
083  public StructureDefinition makeDefinition(String url) {
084    String verSource = url.substring(20, 23);
085    String verTarget = VersionUtilities.getMajMin(context.getVersion());
086    String e = url.substring(54);
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    sd.setUserData("path", PackageHacker.fixPackageUrl("https://hl7.org/fhir/versions.html#extensions"));
096    sd.setUrl(url);
097    sd.setVersion(context.getVersion());
098    sd.setFhirVersion(FHIRVersion.fromCode(context.getVersion()));
099    sd.setKind(StructureDefinitionKind.COMPLEXTYPE);
100    sd.setType("Extension");
101    sd.setDerivation(TypeDerivationRule.CONSTRAINT);
102    sd.setName("Extension-"+verSource+"-"+e);
103    sd.setTitle("Extension Definition for "+e+" for Version "+verSource);
104    sd.setStatus(PublicationStatus.ACTIVE);
105    sd.setExperimental(false);
106    sd.setDate(new Date());
107    sd.setPublisher("FHIR Project");
108    sd.setPurpose("Defined so the validator can validate cross version extensions (see http://hl7.org/fhir/versions.html#extensions)");
109    sd.setAbstract(false);
110    sd.addContext().setType(ExtensionContextType.ELEMENT).setExpression(head(e));
111    sd.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/Extension");
112    if (path.has("types")) {
113      sd.getDifferential().addElement().setPath("Extension.extension").setMax("0");
114      sd.getDifferential().addElement().setPath("Extension.url").setFixed(new UriType(url));
115      ElementDefinition val = sd.getDifferential().addElement().setPath("Extension.value[x]").setMin(1);
116      populateTypes(path, val, verSource, verTarget);
117    } else if (path.has("elements")) {
118      for (JsonElement i : path.forceArray("elements").getItems()) {
119        String apath = e+"."+i.asString();
120        JsonObject elt = root.getJsonObject(apath);
121        if (elt != null) {
122          genExtensionContents(root, apath, verSource, verTarget, sd, i, elt, "Extension.extension");
123        }
124      }      
125      sd.getDifferential().addElement().setPath("Extension.url").setFixed(new UriType(url));
126      sd.getDifferential().addElement().setPath("Extension.value[x]").setMax("0");
127    } else {
128      throw new FHIRException("Internal error - attempt to define extension for "+url+" when it is invalid");
129    }
130    if (path.has("modifier") && path.asBoolean("modifier")) {
131      ElementDefinition baseDef = new ElementDefinition("Extension");
132      sd.getDifferential().getElement().add(0, baseDef);
133      baseDef.setIsModifier(true);
134    }
135    return sd;
136  }
137
138  private void genExtensionContents(JsonObject root, String apath, String verSource, String verTarget, StructureDefinition sd, JsonElement i, JsonObject elt, String epath) {
139    String s = i.asString().replace("[x]", "");
140    sd.getDifferential().addElement().setPath(epath).setSliceName(s);
141    if (elt.has("types")) {            
142      sd.getDifferential().addElement().setPath(epath+".extension").setMax("0");
143      sd.getDifferential().addElement().setPath(epath+".url").setFixed(new UriType(s));
144      ElementDefinition val = sd.getDifferential().addElement().setPath(epath+".value[x]").setMin(1);
145      populateTypes(elt, val, verSource, verTarget);
146    } else if (elt.has("elements")) {
147      for (JsonElement ic : elt.forceArray("elements").getItems()) { 
148        String apathC = apath+"."+ic.asString();
149        JsonObject eltC = root.getJsonObject(apathC);
150        if (eltC != null) {
151          genExtensionContents(root, apathC, verSource, verTarget, sd, ic, eltC, epath+".extension");
152        }
153      }
154      sd.getDifferential().addElement().setPath(epath+".url").setFixed(new UriType(s));
155      sd.getDifferential().addElement().setPath(epath+".value[x]").setMax("0");
156    } else {
157      throw new FHIRException("Internal error - unknown element "+apath);
158    }
159  }
160
161  public void populateTypes(JsonObject path, ElementDefinition val, String verSource, String verTarget) {
162    for (JsonElement i : path.forceArray("types").getItems()) {
163      String s = i.asString();
164      if (!s.startsWith("!")) {
165        if (s.contains("(")) {
166          String t = s.substring(0, s.indexOf("("));
167          TypeRefComponent tr = val.addType().setCode(translateDataType(verTarget, t));
168          if (hasTargets(tr.getCode()) ) {
169            s = s.substring(t.length()+1);
170            for (String p : s.substring(0, s.length()-1).split("\\|")) {
171              if ("Any".equals(p)) {
172                tr.addTargetProfile("http://hl7.org/fhir/StructureDefinition/Resource");
173              } else if (p.contains(",")) {
174                for (String pp : p.split("\\,")) {
175                  if (isResource(pp)) {
176                    tr.addTargetProfile("http://hl7.org/fhir/StructureDefinition/"+pp);
177                  }
178                }
179              } else if (isResource(p)) {
180                tr.addTargetProfile("http://hl7.org/fhir/StructureDefinition/"+p);              
181              }
182            }
183          }
184        } else {
185          val.addType().setCode(translateDataType(verTarget, s));
186        }
187      }
188    }
189  }
190
191  // todo: translate names 
192  
193  private boolean isResource(String p) {
194    return context.getResourceNames().contains(p);
195  }
196
197  private boolean hasTargets(String dt) {
198    return Utilities.existsInList(dt, "canonical", "Reference", "CodeableReference");
199  }
200
201  private String translateDataType(String v, String dt) {
202    if (VersionUtilities.versionsCompatible("1.0", v) || VersionUtilities.versionsCompatible("1.4", v)) {
203      return translateToR2(dt);
204    } else if (VersionUtilities.versionsCompatible("3.0", v)) {
205      return translateToR3(dt);
206    } else {
207      return dt;
208    }
209  }
210
211  private String translateToR3(String dt) {
212    if ("canonical".equals(dt)) {
213      return "uri";
214    } else if ("url".equals(dt)) {
215      return "uri";
216    } else {
217      return dt;
218    }
219  }
220
221  private String translateToR2(String dt) {
222    if ("canonical".equals(dt)) {
223      return "uri";
224    } else if ("url".equals(dt)) {
225      return "uri";
226    } else if ("uuid".equals(dt)) {
227      return "id";
228    } else {
229      return dt;
230    }
231  }
232
233  private String head(String id) {
234    if (id.contains(".")) {
235      return id.substring(0, id.lastIndexOf("."));
236    } else {
237      return id;
238    }
239  }
240  
241  public String getVersion(String url) {
242    return url.substring(20, 23);
243  }
244
245  public boolean matchingUrl(String url) {
246    if (url == null || url.length() < 56) {
247      return false;
248    }
249    String pfx = url.substring(0, 20);
250    String v = url.substring(20, 23);
251    String sfx = url.substring(23, 54);
252    return pfx.equals("http://hl7.org/fhir/") &&
253       isVersionPattern(v) && sfx.equals("/StructureDefinition/extension-");
254  }
255
256  private boolean isVersionPattern(String v) {
257    return v.length() == 3 && Character.isDigit(v.charAt(0)) && v.charAt(1) == '.' && Character.isDigit(v.charAt(2));
258  }
259
260}