001package org.hl7.fhir.convertors;
002
003import java.io.IOException;
004import java.io.InputStream;
005import java.text.SimpleDateFormat;
006import java.util.ArrayList;
007import java.util.Calendar;
008import java.util.GregorianCalendar;
009import java.util.HashSet;
010import java.util.List;
011import java.util.Locale;
012import java.util.Set;
013import java.util.TimeZone;
014
015import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_10_40;
016import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_30_40;
017import org.hl7.fhir.convertors.factory.VersionConvertorFactory_10_40;
018import org.hl7.fhir.convertors.factory.VersionConvertorFactory_14_40;
019import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_40;
020import org.hl7.fhir.convertors.loaders.loaderR4.R2016MayToR4Loader;
021import org.hl7.fhir.convertors.loaders.loaderR4.R2ToR4Loader;
022import org.hl7.fhir.convertors.loaders.loaderR4.R3ToR4Loader;
023import org.hl7.fhir.convertors.misc.IGR2ConvertorAdvisor;
024import org.hl7.fhir.exceptions.FHIRException;
025import org.hl7.fhir.r4.conformance.ProfileUtilities;
026import org.hl7.fhir.r4.context.BaseWorkerContext;
027import org.hl7.fhir.r4.context.SimpleWorkerContext;
028import org.hl7.fhir.r4.formats.JsonParser;
029import org.hl7.fhir.r4.model.ElementDefinition;
030import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent;
031import org.hl7.fhir.r4.model.Enumerations.FHIRVersion;
032import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
033import org.hl7.fhir.r4.model.ImplementationGuide.SPDXLicense;
034import org.hl7.fhir.r4.model.Resource;
035import org.hl7.fhir.r4.model.StructureDefinition;
036import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind;
037import org.hl7.fhir.r4.model.StructureDefinition.TypeDerivationRule;
038import org.hl7.fhir.r4.model.UriType;
039import org.hl7.fhir.r4.utils.NPMPackageGenerator;
040import org.hl7.fhir.r4.utils.NPMPackageGenerator.Category;
041import org.hl7.fhir.utilities.Utilities;
042import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
043import org.hl7.fhir.utilities.npm.NpmPackage;
044import org.hl7.fhir.utilities.npm.PackageGenerator.PackageType;
045import org.hl7.fhir.utilities.npm.ToolsVersion;
046
047/*
048  Copyright (c) 2011+, HL7, Inc.
049  All rights reserved.
050  
051  Redistribution and use in source and binary forms, with or without modification, 
052  are permitted provided that the following conditions are met:
053    
054   * Redistributions of source code must retain the above copyright notice, this 
055     list of conditions and the following disclaimer.
056   * Redistributions in binary form must reproduce the above copyright notice, 
057     this list of conditions and the following disclaimer in the documentation 
058     and/or other materials provided with the distribution.
059   * Neither the name of HL7 nor the names of its contributors may be used to 
060     endorse or promote products derived from this software without specific 
061     prior written permission.
062  
063  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
064  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
065  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
066  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
067  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
068  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
069  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
070  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
071  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
072  POSSIBILITY OF SUCH DAMAGE.
073  
074 */
075
076
077import com.google.gson.JsonArray;
078import com.google.gson.JsonObject;
079
080@SuppressWarnings("checkstyle:systemout")
081public class ExtensionDefinitionGenerator {
082
083  private FHIRVersion sourceVersion;
084  private FHIRVersion targetVersion;
085  private String filename;
086  private StructureDefinition extbase;
087  private ElementDefinition extv;
088  private ProfileUtilities pu;
089  private BaseWorkerContext context;
090
091  public static void main(String[] args) throws IOException, FHIRException {
092    if (args.length == 0) {
093      System.out.println("Extension Generator");
094      System.out.println("===================");
095      System.out.println();
096      System.out.println("See http://hl7.org/fhir/versions.html#extensions. This generates the packages");
097      System.out.println();
098      System.out.println("parameters: -srcver [version] -tgtver [version] -package [filename]");
099      System.out.println();
100      System.out.println("srcver: the source version to load");
101      System.out.println("tgtver: the version to generate extension definitions for");
102      System.out.println("package: the package to produce");
103    } else {
104      ExtensionDefinitionGenerator self = new ExtensionDefinitionGenerator();
105      self.setSourceVersion(FHIRVersion.fromCode(getNamedParam(args, "-srcver")));
106      self.setTargetVersion(FHIRVersion.fromCode(getNamedParam(args, "-tgtver")));
107      self.setFilename(getNamedParam(args, "-package"));
108      self.generate();
109    }
110  }
111
112  private static String getNamedParam(String[] args, String param) {
113    boolean found = false;
114    for (String a : args) {
115      if (found)
116        return a;
117      if (a.equals(param)) {
118        found = true;
119      }
120    }
121    throw new Error("Unable to find parameter " + param);
122  }
123
124  public FHIRVersion getSourceVersion() {
125    return sourceVersion;
126  }
127
128  public void setSourceVersion(FHIRVersion sourceVersion) {
129    this.sourceVersion = sourceVersion;
130  }
131
132  public FHIRVersion getTargetVersion() {
133    return targetVersion;
134  }
135
136  public void setTargetVersion(FHIRVersion targetVersion) {
137    this.targetVersion = targetVersion;
138  }
139
140  public String getFilename() {
141    return filename;
142  }
143
144  public void setFilename(String filename) {
145    this.filename = filename;
146  }
147
148
149  private void generate() throws IOException, FHIRException {
150    List<StructureDefinition> definitions = loadSource();
151    List<StructureDefinition> extensions = buildExtensions(definitions);
152    for (StructureDefinition ext : extensions)
153      pu.generateSnapshot(extbase, ext, ext.getUrl(), "http://hl7.org/fhir/R4", ext.getName());
154    savePackage(extensions);
155
156  }
157
158  private List<StructureDefinition> buildExtensions(List<StructureDefinition> definitions) throws FHIRException {
159    Set<String> types = new HashSet<>();
160    List<StructureDefinition> list = new ArrayList<>();
161    for (StructureDefinition type : definitions)
162      if (type.getDerivation() == TypeDerivationRule.SPECIALIZATION && !type.getName().contains(".") && !types.contains(type.getName()) && type.getKind() != StructureDefinitionKind.PRIMITIVETYPE && !Utilities.existsInList(type.getName(), "Extension", "Narrative")) {
163        types.add(type.getName());
164        buildExtensions(type, list);
165      }
166    return list;
167  }
168
169
170  private void buildExtensions(StructureDefinition type, List<StructureDefinition> list) throws FHIRException {
171    for (ElementDefinition ed : type.getDifferential().getElement()) {
172      if (ed.getPath().contains(".")) {
173        if (!ed.getPath().endsWith(".extension") && !ed.getPath().endsWith(".modifierExtension")) {
174          StructureDefinition ext = generateExtension(type, ed);
175          if (ext != null) {
176            list.add(ext);
177            context.cacheResource(ext);
178          }
179        }
180      }
181    }
182  }
183
184  private StructureDefinition generateExtension(StructureDefinition type, ElementDefinition ed) throws FHIRException {
185    StructureDefinition ext = new StructureDefinition();
186    ext.setId("extension-" + ed.getPath().replace("[x]", ""));
187    ext.setUrl("http://hl7.org/fhir/" + sourceVersion.toCode().substring(0, 3) + "/StructureDefinition/" + ext.getId());
188    if (ext.getId().length() > 64)
189      ext.setId(contract(ext.getId()));
190    ext.setVersion(sourceVersion.toCode());
191    ext.setName("ExtensionR" + sourceVersion.toCode().substring(0, 1) + ed.getPath().replace(".", ""));
192    ext.setTitle("Extension definition for R" + sourceVersion.toCode().substring(0, 1) + " element " + ed.getPath());
193    ext.setStatus(PublicationStatus.ACTIVE);
194    ext.setDate(type.getDate());
195    ext.setFhirVersion(type.getFhirVersion());
196    ext.setDescription(ed.getDefinition());
197    ext.setKind(StructureDefinitionKind.COMPLEXTYPE);
198    ext.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/Extension");
199    ext.setDerivation(TypeDerivationRule.CONSTRAINT);
200    if (ed.hasType() && ("Element".equals(ed.getType().get(0).getCode()) || "BackboneElement".equals(ed.getType().get(0).getCode()))) {
201      ElementDefinition v = ed.copy();
202      v.setPath("Extension");
203      v.getType().clear();
204      v.setIsSummaryElement(null);
205      ext.getDifferential().addElement(v);
206      List<ElementDefinition> children = ProfileUtilities.getChildList(type, ed);
207      for (ElementDefinition child : children) {
208        String n = tail(child.getPath());
209        if (!Utilities.existsInList(n, "id", "extension", "modifierExtension") && !hasNonValidType(child)) {
210          v = child.copy();
211          v.setId("Extension.extension:" + n);
212          v.setPath("Extension.extension");
213          v.setSliceName(n);
214          v.getType().clear();
215          v.setIsSummaryElement(null);
216          v.addType().setCode("Extension").addProfile("http://hl7.org/fhir/" + sourceVersion.toCode().substring(0, 3) + "/StructureDefinition/extension-" + child.getPath().replace("[x]", ""));
217          ext.getDifferential().addElement(v);
218        }
219      }
220      ext.getDifferential().addElement(genElement("Extension.url").setFixed(new UriType(ext.getUrl())));
221      ext.getDifferential().addElement(genElement("Extension.value[x]").setMax("0"));
222
223    } else if (ed.hasType() && Utilities.existsInList(ed.getType().get(0).getCode(), "Resource", "Narrative")) {
224      return null;
225    } else if (ed.hasType() && !goesInExtension(ed.getType().get(0).getCode())) {
226      ElementDefinition v = ed.copy();
227      v.setPath("Extension");
228      v.getType().clear();
229      v.setIsSummaryElement(null);
230      ext.getDifferential().addElement(v);
231      List<ElementDefinition> children = ProfileUtilities.getChildList(type, ed);
232      for (ElementDefinition child : children) {
233        String n = tail(child.getPath());
234        if (!Utilities.existsInList(n, "id", "extension", "modifierExtension") && !hasNonValidType(child)) {
235          v = child.copy();
236          v.setId("Extension.extension:" + n);
237          v.setPath("Extension.extension");
238          v.setSliceName(n);
239          v.getType().clear();
240          v.setIsSummaryElement(null);
241          v.addType().setCode("Extension").addProfile("http://hl7.org/fhir/" + sourceVersion.toCode().substring(0, 3) + "/StructureDefinition/extension-" + child.getPath().replace("[x]", ""));
242          ext.getDifferential().addElement(v);
243        }
244      }
245      ext.getDifferential().addElement(genElement("Extension.url").setFixed(new UriType(ext.getUrl())));
246      ext.getDifferential().addElement(genElement("Extension.value[x]").setMax("0"));
247    } else {
248      // simple type...
249      ElementDefinition v = ed.copy();
250      v.setPath("Extension");
251      v.getType().clear();
252      v.setIsSummaryElement(null);
253      ext.getDifferential().addElement(v);
254      ext.getDifferential().addElement(genElement("Extension.extension").setMax("0"));
255      ext.getDifferential().addElement(genElement("Extension.url").setFixed(new UriType(ext.getUrl())));
256      v = ed.copy();
257      v.setPath("Extension.value[x]");
258      v.setId("Extension.value");
259      v.setMax("1");
260      v.setIsSummaryElement(null);
261      ext.getDifferential().addElement(v);
262    }
263    return ext;
264  }
265
266  private boolean hasNonValidType(ElementDefinition ed) {
267    return ed.hasType() && Utilities.existsInList(ed.getType().get(0).getCode(), "Resource", "Narrative");
268  }
269
270
271  private boolean goesInExtension(String code) {
272    if (code == null)
273      return true;
274    for (TypeRefComponent tr : extv.getType()) {
275      if (code.equals(tr.getCode()))
276        return true;
277    }
278    return false;
279  }
280
281
282  private String tail(String path) {
283    return path.substring(path.lastIndexOf(".") + 1);
284  }
285
286
287  private ElementDefinition genElement(String path) {
288    return new ElementDefinition().setPath(path);
289  }
290
291
292  private String contract(String id) {
293    List<StringReplacement> abbrevs = new ArrayList<>();
294    abbrevs.add(new StringReplacement("AdverseEvent", "AE"));
295    abbrevs.add(new StringReplacement("CoverageEligibilityResponse", "CERsp"));
296    abbrevs.add(new StringReplacement("CoverageEligibilityRequest", "CEReq"));
297    abbrevs.add(new StringReplacement("EffectEvidenceSynthesis", "EES"));
298    abbrevs.add(new StringReplacement("ExplanationOfBenefit", "EoB"));
299    abbrevs.add(new StringReplacement("ImmunizationRecommendation", "IR"));
300    abbrevs.add(new StringReplacement("MeasureReport", "MR"));
301    abbrevs.add(new StringReplacement("MedicationKnowledge", "MK"));
302    abbrevs.add(new StringReplacement("CapabilityStatement", "CS"));
303    abbrevs.add(new StringReplacement("ChargeItemDefinition", "CID"));
304    abbrevs.add(new StringReplacement("ClaimResponse", "CR"));
305    abbrevs.add(new StringReplacement("InsurancePlan", "IP"));
306    abbrevs.add(new StringReplacement("MedicationRequest", "MR"));
307    abbrevs.add(new StringReplacement("MedicationOrder", "MO"));
308    abbrevs.add(new StringReplacement("MedicationDispense", "MD"));
309    abbrevs.add(new StringReplacement("NutritionOrder", "NO"));
310    abbrevs.add(new StringReplacement("MedicinalProductAuthorization", "MPA"));
311    abbrevs.add(new StringReplacement("MedicinalProductContraindication", "MPC"));
312    abbrevs.add(new StringReplacement("MedicinalProductIngredient", "MPI"));
313    abbrevs.add(new StringReplacement("MedicinalProductPharmaceutical", "MPP"));
314    abbrevs.add(new StringReplacement("MedicinalProduct", "MP"));
315    abbrevs.add(new StringReplacement("ResearchElementDefinition", "RED"));
316    abbrevs.add(new StringReplacement("RiskEvidenceSynthesis", "RES"));
317    abbrevs.add(new StringReplacement("ObservationDefinition", "OD"));
318    abbrevs.add(new StringReplacement("SubstanceReferenceInformation", "SRI"));
319    abbrevs.add(new StringReplacement("SubstanceSourceMaterial", "SSM"));
320    abbrevs.add(new StringReplacement("SpecimenDefinition", "SD"));
321    abbrevs.add(new StringReplacement("SubstanceSpecification", "SS"));
322    abbrevs.add(new StringReplacement("SubstancePolymer", "SP"));
323    abbrevs.add(new StringReplacement("TerminologyCapabilities", "TC"));
324    abbrevs.add(new StringReplacement("VerificationResult", "VR"));
325    abbrevs.add(new StringReplacement("EligibilityResponse", "ERsp"));
326    abbrevs.add(new StringReplacement("ExpansionProfile", "EP"));
327    abbrevs.add(new StringReplacement("ImagingObjectSelection", "IOS"));
328
329
330    abbrevs.add(new StringReplacement("administrationGuidelines.patientCharacteristics", "ag.pc"));
331    abbrevs.add(new StringReplacement("manufacturingBusinessOperation", "mbo"));
332    abbrevs.add(new StringReplacement("strength.referenceStrength", "strength.rs"));
333    abbrevs.add(new StringReplacement("MPP.routeOfAdministration", "MPP.roa"));
334    abbrevs.add(new StringReplacement("supportingInformation", "si"));
335    abbrevs.add(new StringReplacement("structuralRepresentation", "sr"));
336    abbrevs.add(new StringReplacement("compareToSourceExpression", "ctse"));
337    abbrevs.add(new StringReplacement("TestScript.setup.action.assert", "TestScript.s.a.a"));
338
339    for (StringReplacement s : abbrevs)
340      if (id.contains(s.getSource()))
341        id = id.replace(s.getSource(), s.getReplacement());
342    if (id.length() > 64)
343      throw new Error("Still too long: " + id);
344    return id;
345  }
346
347
348  private String timezone() {
349    TimeZone tz = TimeZone.getDefault();
350    Calendar cal = GregorianCalendar.getInstance(tz);
351    int offsetInMillis = tz.getOffset(cal.getTimeInMillis());
352
353    String offset = String.format("%02d:%02d", Math.abs(offsetInMillis / 3600000), Math.abs((offsetInMillis / 60000) % 60));
354    offset = (offsetInMillis >= 0 ? "+" : "-") + offset;
355
356    return offset;
357  }
358
359  private void savePackage(List<StructureDefinition> extensions) throws FHIRException, IOException {
360    JsonObject npm = new JsonObject();
361    npm.addProperty("name", "hl7.fhir.extensions.r" + sourceVersion.toCode().substring(0, 1));
362    npm.addProperty("version", targetVersion.toCode().substring(0, 3));
363    npm.addProperty("tools-version", ToolsVersion.TOOLS_VERSION);
364    npm.addProperty("type", PackageType.IG.getCode());
365    npm.addProperty("license", SPDXLicense.CC0_1_0.toCode());
366    npm.addProperty("canonical", "http://hl7.org/fhir/" + sourceVersion.toCode().substring(0, 3) + "/extensions/" + targetVersion.toCode().substring(0, 3));
367    npm.addProperty("url", "http://hl7.org/fhir/" + sourceVersion.toCode().substring(0, 3) + "/extensions/" + targetVersion.toCode().substring(0, 3));
368    npm.addProperty("title", "Extension Definitions for representing elements from " + sourceVersion.toCode() + " in " + targetVersion.toCode());
369    npm.addProperty("description", "Extension Definitions for representing elements from " + sourceVersion.toCode() + " in " + targetVersion.toCode() + " built " + new SimpleDateFormat("EEE, MMM d, yyyy HH:mmZ", new Locale("en", "US")).format(Calendar.getInstance().getTime()) + timezone() + ")");
370    JsonObject dep = new JsonObject();
371    npm.add("dependencies", dep);
372    dep.addProperty("hl7.fhir.core", targetVersion.toCode());
373    npm.addProperty("author", "FHIR Project");
374    JsonArray m = new JsonArray();
375    JsonObject md = new JsonObject();
376    m.add(md);
377    md.addProperty("name", "FHIR Project");
378    md.addProperty("url", "http://hl7.org/fhir");
379    NPMPackageGenerator pi = new NPMPackageGenerator(filename, npm);
380    for (StructureDefinition sd : extensions) {
381      byte[] cnt = saveResource(sd, targetVersion);
382      pi.addFile(Category.RESOURCE, "StructureDefinition-" + sd.getId() + ".json", cnt);
383    }
384    pi.finish();
385
386  }
387
388
389  private List<StructureDefinition> loadSource() throws IOException, FHIRException {
390    List<StructureDefinition> list = new ArrayList<>();
391    FilesystemPackageCacheManager pcm = new FilesystemPackageCacheManager.Builder().build();
392    NpmPackage npm = pcm.loadPackage("hl7.fhir.core", sourceVersion.toCode());
393    if (sourceVersion == FHIRVersion._4_0_0)
394      context = SimpleWorkerContext.fromPackage(npm);
395    else if (sourceVersion == FHIRVersion._3_0_1)
396      context = SimpleWorkerContext.fromPackage(npm, new R3ToR4Loader());
397    else if (sourceVersion == FHIRVersion._1_4_0)
398      context = SimpleWorkerContext.fromPackage(npm, new R2016MayToR4Loader());
399    else if (sourceVersion == FHIRVersion._1_0_2)
400      context = SimpleWorkerContext.fromPackage(npm, new R2ToR4Loader());
401    pu = new ProfileUtilities(context, null, null);
402    for (String fn : npm.listResources("StructureDefinition")) {
403      list.add((StructureDefinition) loadResource(npm.load("package", fn), sourceVersion));
404    }
405    for (StructureDefinition sd : list)
406      if (sd.getName().equals("Extension")) {
407        extbase = sd;
408        extv = extbase.getSnapshot().getElement().get(extbase.getSnapshot().getElement().size() - 1);
409      }
410    return list;
411  }
412
413  private byte[] saveResource(Resource resource, FHIRVersion v) throws IOException, FHIRException {
414    if (v == FHIRVersion._3_0_1) {
415      org.hl7.fhir.dstu3.model.Resource res = VersionConvertorFactory_30_40.convertResource(resource, new BaseAdvisor_30_40(false));
416      return new org.hl7.fhir.dstu3.formats.JsonParser().composeBytes(res);
417    } else if (v == FHIRVersion._1_4_0) {
418      org.hl7.fhir.dstu2016may.model.Resource res = VersionConvertorFactory_14_40.convertResource(resource);
419      return new org.hl7.fhir.dstu2016may.formats.JsonParser().composeBytes(res);
420    } else if (v == FHIRVersion._1_0_2) {
421      BaseAdvisor_10_40 advisor = new IGR2ConvertorAdvisor();
422      org.hl7.fhir.dstu2.model.Resource res = VersionConvertorFactory_10_40.convertResource(resource, advisor);
423      return new org.hl7.fhir.dstu2.formats.JsonParser().composeBytes(res);
424    } else if (v == FHIRVersion._4_0_0) {
425      return new JsonParser().composeBytes(resource);
426    } else
427      throw new Error("Unsupported version " + v);
428  }
429
430  private Resource loadResource(InputStream inputStream, FHIRVersion v) throws IOException, FHIRException {
431    if (v == FHIRVersion._3_0_1) {
432      org.hl7.fhir.dstu3.model.Resource res = new org.hl7.fhir.dstu3.formats.JsonParser().parse(inputStream);
433      return VersionConvertorFactory_30_40.convertResource(res, new BaseAdvisor_30_40(false));
434    } else if (v == FHIRVersion._1_4_0) {
435      org.hl7.fhir.dstu2016may.model.Resource res = new org.hl7.fhir.dstu2016may.formats.JsonParser().parse(inputStream);
436      return VersionConvertorFactory_14_40.convertResource(res);
437    } else if (v == FHIRVersion._1_0_2) {
438      org.hl7.fhir.dstu2.model.Resource res = new org.hl7.fhir.dstu2.formats.JsonParser().parse(inputStream);
439      BaseAdvisor_10_40 advisor = new IGR2ConvertorAdvisor();
440      return VersionConvertorFactory_10_40.convertResource(res, advisor);
441    } else if (v == FHIRVersion._4_0_0) {
442      return new JsonParser().parse(inputStream);
443    } else
444      throw new Error("Unsupported version " + v);
445  }
446}