001package org.hl7.fhir.convertors;
002
003import java.io.ByteArrayInputStream;
004import java.io.ByteArrayOutputStream;
005import java.io.File;
006import java.io.FileInputStream;
007import java.io.FileOutputStream;
008import java.io.IOException;
009import java.io.InputStream;
010import java.io.OutputStream;
011import java.util.ArrayList;
012import java.util.HashMap;
013import java.util.List;
014import java.util.Map;
015import java.util.UUID;
016import java.util.zip.ZipEntry;
017import java.util.zip.ZipInputStream;
018
019/*
020  Copyright (c) 2011+, HL7, Inc.
021  All rights reserved.
022  
023  Redistribution and use in source and binary forms, with or without modification, 
024  are permitted provided that the following conditions are met:
025    
026   * Redistributions of source code must retain the above copyright notice, this 
027     list of conditions and the following disclaimer.
028   * Redistributions in binary form must reproduce the above copyright notice, 
029     this list of conditions and the following disclaimer in the documentation 
030     and/or other materials provided with the distribution.
031   * Neither the name of HL7 nor the names of its contributors may be used to 
032     endorse or promote products derived from this software without specific 
033     prior written permission.
034  
035  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
036  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
037  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
038  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
039  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
040  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
041  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
042  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
043  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
044  POSSIBILITY OF SUCH DAMAGE.
045  
046 */
047
048import org.hl7.fhir.convertors.loaders.loaderR3.R2ToR3Loader;
049import org.hl7.fhir.dstu3.context.SimpleWorkerContext;
050import org.hl7.fhir.dstu3.elementmodel.Manager;
051import org.hl7.fhir.dstu3.elementmodel.Manager.FhirFormat;
052import org.hl7.fhir.dstu3.formats.FormatUtilities;
053import org.hl7.fhir.dstu3.formats.IParser.OutputStyle;
054import org.hl7.fhir.dstu3.formats.JsonParser;
055import org.hl7.fhir.dstu3.model.Base;
056import org.hl7.fhir.dstu3.model.Coding;
057import org.hl7.fhir.dstu3.model.ExpansionProfile;
058import org.hl7.fhir.dstu3.model.MetadataResource;
059import org.hl7.fhir.dstu3.model.PractitionerRole;
060import org.hl7.fhir.dstu3.model.Resource;
061import org.hl7.fhir.dstu3.model.ResourceFactory;
062import org.hl7.fhir.dstu3.model.StructureDefinition;
063import org.hl7.fhir.dstu3.model.StructureDefinition.StructureDefinitionKind;
064import org.hl7.fhir.dstu3.model.StructureMap;
065import org.hl7.fhir.dstu3.model.UriType;
066import org.hl7.fhir.dstu3.utils.StructureMapUtilities;
067import org.hl7.fhir.dstu3.utils.StructureMapUtilities.ITransformerServices;
068import org.hl7.fhir.exceptions.FHIRException;
069import org.hl7.fhir.r5.utils.ToolingExtensions;
070import org.hl7.fhir.utilities.FileUtilities;
071import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
072
073/**
074 * This class manages conversion from R2 to R3 and vice versa
075 * <p>
076 * To use this class, do the following:
077 * <p>
078 * - provide a stream or path (file or URL) that points to R2 definitions (from http://hl7.org/fhir/DSTU2/downloads.html)
079 * - provide a stream or a path (file or URL) that points to the R3 definitions  (from http://hl7.org/fhir/STU3/downloads.html)
080 * - provide a stream or a path (file or URL) that points to R2/R3 map files (from http://hl7.org/fhir/r2r3maps.zip)
081 * <p>
082 * - call convert() (can call this more than once, but not multithread safe)
083 *
084 * @author Grahame Grieve
085 */
086@SuppressWarnings("checkstyle:systemout")
087public class R2R3ConversionManager implements ITransformerServices {
088
089  private final Map<String, StructureMap> library = new HashMap<String, StructureMap>();
090  private final List<Resource> extras = new ArrayList<Resource>();
091  private SimpleWorkerContext contextR2;
092  private SimpleWorkerContext contextR3;
093  private boolean needPrepare = false;
094  private StructureMapUtilities smu3;
095  private StructureMapUtilities smu2;
096  private OutputStyle style = OutputStyle.PRETTY;
097
098  public static void main(String[] args) throws IOException, FHIRException {
099    if (args.length == 0 || !hasParam(args, "-d2") || !hasParam(args, "-d3") || !hasParam(args, "-maps") || !hasParam(args, "-src") || !hasParam(args, "-dest") || (!hasParam(args, "-r2") && !hasParam(args, "-r3"))) {
100      System.out.println("R2 <--> R3 Convertor");
101      System.out.println("====================");
102      System.out.println();
103      System.out.println("parameters: -d2 [r2 definitions] -d3 [r3 definitions] -maps [map source] -src [source] -dest [dest] -r2/3 - fmt [format]");
104      System.out.println();
105      System.out.println("d2: definitions from http://hl7.org/fhir/DSTU2/downloads.html");
106      System.out.println("d3: definitions from http://hl7.org/fhir/STU3/downloads.html");
107      System.out.println("maps: R2/R3 maps from http://hl7.org/fhir/r2r3maps.zip");
108      System.out.println("src: filename for source to convert");
109      System.out.println("dest: filename for destination of conversion");
110      System.out.println("-r2: source is r2, convert to r3");
111      System.out.println("-r3: source is r3, convert to r2");
112      System.out.println("-fmt: xml | json (xml is default)");
113    } else {
114      R2R3ConversionManager self = new R2R3ConversionManager();
115      self.setR2Definitions(getNamedParam(args, "-d2"));
116      self.setR3Definitions(getNamedParam(args, "-d3"));
117      self.setMappingLibrary(getNamedParam(args, "-maps"));
118      FhirFormat fmt = hasParam(args, "-fmt") ? getNamedParam(args, "-fmt").equalsIgnoreCase("json") ? FhirFormat.JSON : FhirFormat.XML : FhirFormat.XML;
119      InputStream src = ManagedFileAccess.inStream(getNamedParam(args, "-src"));
120      OutputStream dst = ManagedFileAccess.outStream(getNamedParam(args, "-dest"));
121      self.convert(src, dst, hasParam(args, "-r2"), fmt);
122    }
123  }
124
125  private static boolean hasParam(String[] args, String param) {
126    for (String a : args)
127      if (a.equals(param))
128        return true;
129    return false;
130  }
131
132  private static String getNamedParam(String[] args, String param) {
133    boolean found = false;
134    for (String a : args) {
135      if (found)
136        return a;
137      if (a.equals(param)) {
138        found = true;
139      }
140    }
141    return null;
142  }
143
144  public OutputStyle getStyle() {
145    return style;
146  }
147
148  public void setStyle(OutputStyle style) {
149    this.style = style;
150  }
151
152  public List<Resource> getExtras() {
153    return extras;
154  }
155
156  // set up ------------------------------------------------------------------
157  public void setR2Definitions(InputStream stream) throws IOException, FHIRException {
158    needPrepare = true;
159    R2ToR3Loader ldr = new R2ToR3Loader();
160    ldr.setPatchUrls(true).setKillPrimitives(true);
161    Map<String, InputStream> files = readInputStream(stream);
162    contextR2 = new SimpleWorkerContext();
163    contextR2.setAllowLoadingDuplicates(true);
164    contextR2.loadFromFile(files.get("profiles-types.xml"), "profiles-types.xml", ldr);
165    contextR2.loadFromFile(files.get("profiles-resources.xml"), "profiles-resources.xml", ldr);
166    contextR2.loadFromFile(files.get("valuesets.xml"), "valuesets.xml", ldr);
167  }
168
169  public void setR2Definitions(String source) throws IOException, FHIRException {
170    File f = ManagedFileAccess.file(source);
171    if (f.exists())
172      setR2Definitions(ManagedFileAccess.inStream(f));
173    else
174      setR2Definitions(fetch(source));
175  }
176
177  public void setR3Definitions(InputStream stream) throws IOException, FHIRException {
178    needPrepare = true;
179    Map<String, InputStream> files = readInputStream(stream);
180    contextR3 = new SimpleWorkerContext();
181    contextR2.setAllowLoadingDuplicates(true);
182    contextR3.loadFromFile(files.get("profiles-types.xml"), "profiles-types.xml", null);
183    contextR3.loadFromFile(files.get("profiles-resources.xml"), "profiles-resources.xml", null);
184    contextR3.loadFromFile(files.get("extension-definitions.xml"), "extension-definitions.xml", null);
185    contextR3.loadFromFile(files.get("valuesets.xml"), "valuesets.xml", null);
186    contextR3.setCanRunWithoutTerminology(true);
187  }
188
189  public void setR3Definitions(String source) throws IOException, FHIRException {
190    File f = ManagedFileAccess.file(source);
191    if (f.exists())
192      setR3Definitions(ManagedFileAccess.inStream(f));
193    else
194      setR3Definitions(fetch(source));
195  }
196
197  public void setMappingLibrary(InputStream stream) throws IOException, FHIRException {
198    needPrepare = true;
199    Map<String, InputStream> files = readInputStream(stream);
200    for (InputStream s : files.values()) {
201      StructureMap sm = new StructureMapUtilities(contextR3).parse(FileUtilities.streamToString(s));
202      library.put(sm.getUrl(), sm);
203    }
204  }
205
206  public void setMappingLibrary(String source) throws IOException, FHIRException {
207    File f = ManagedFileAccess.file(source);
208    if (f.exists())
209      setMappingLibrary(ManagedFileAccess.inStream(f));
210    else
211      setMappingLibrary(fetch(source));
212  }
213
214  // support
215  private InputStream fetch(String source) {
216    throw new Error("not done yet");
217  }
218
219  private Map<String, InputStream> readInputStream(InputStream stream) throws IOException {
220    Map<String, InputStream> res = new HashMap<String, InputStream>();
221    ZipInputStream zip = new ZipInputStream(stream);
222    ZipEntry ze = null;
223    while ((ze = zip.getNextEntry()) != null) {
224      String n = ze.getName();
225      ByteArrayOutputStream bs = new ByteArrayOutputStream();
226      for (int c = zip.read(); c != -1; c = zip.read()) {
227        bs.write(c);
228      }
229      bs.close();
230      res.put(n, new ByteArrayInputStream(bs.toByteArray()));
231      zip.closeEntry();
232    }
233    zip.close();
234    return res;
235  }
236
237  private void prepare() throws FHIRException {
238    if (contextR2 == null)
239      throw new FHIRException("No R2 definitions provided");
240    if (contextR3 == null)
241      throw new FHIRException("No R3 definitions provided");
242    if (library == null)
243      throw new FHIRException("No R2/R# conversion maps provided");
244
245    if (needPrepare) {
246      for (StructureDefinition sd : contextR2.allStructures()) {
247        StructureDefinition sdn = sd.copy();
248        sdn.getExtension().clear();
249        contextR3.seeResource(sdn.getUrl(), sdn);
250      }
251
252      for (StructureDefinition sd : contextR3.allStructures()) {
253        if (sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE) {
254          contextR2.seeResource(sd.getUrl(), sd);
255          StructureDefinition sdn = sd.copy();
256          sdn.setUrl(sdn.getUrl().replace("http://hl7.org/fhir/", "http://hl7.org/fhir/DSTU2/"));
257          sdn.addExtension().setUrl(ToolingExtensions.EXT_XML_NAMESPACE_DEPRECATED).setValue(new UriType("http://hl7.org/fhir"));
258          contextR2.seeResource(sdn.getUrl(), sdn);
259          contextR3.seeResource(sdn.getUrl(), sdn);
260        }
261      }
262
263      contextR2.setExpansionProfile(new ExpansionProfile().setUrl("urn:uuid:" + UUID.randomUUID().toString().toLowerCase()));
264      contextR3.setExpansionProfile(new ExpansionProfile().setUrl("urn:uuid:" + UUID.randomUUID().toString().toLowerCase()));
265
266      smu3 = new StructureMapUtilities(contextR3, library, this);
267      smu2 = new StructureMapUtilities(contextR2, library, this);
268
269      needPrepare = false;
270    }
271  }
272
273  // execution
274  public byte[] convert(byte[] source, boolean r2ToR3, FhirFormat format) throws FHIRException, IOException {
275    prepare();
276    ByteArrayOutputStream bs = new ByteArrayOutputStream();
277    if (r2ToR3)
278      convertToR3(new ByteArrayInputStream(source), bs, format);
279    else
280      convertToR2(new ByteArrayInputStream(source), bs, format);
281    bs.close();
282    return bs.toByteArray();
283  }
284
285  public void convert(InputStream source, OutputStream dest, boolean r2ToR3, FhirFormat format) throws FHIRException, IOException {
286    prepare();
287    if (r2ToR3)
288      convertToR3(source, dest, format);
289    else
290      convertToR2(source, dest, format);
291  }
292
293  public org.hl7.fhir.dstu2.model.Resource convert(org.hl7.fhir.dstu3.model.Resource source) throws IOException, FHIRException {
294    ByteArrayOutputStream bs = new ByteArrayOutputStream();
295    new JsonParser().compose(bs, source);
296    bs.close();
297    return new org.hl7.fhir.dstu2.formats.JsonParser().parse(convert(bs.toByteArray(), false, FhirFormat.JSON));
298  }
299
300  public org.hl7.fhir.dstu3.model.Resource convert(org.hl7.fhir.dstu2.model.Resource source) throws IOException, FHIRException {
301    ByteArrayOutputStream bs = new ByteArrayOutputStream();
302    new org.hl7.fhir.dstu2.formats.JsonParser().compose(bs, source);
303    bs.close();
304    return new JsonParser().parse(convert(bs.toByteArray(), false, FhirFormat.JSON));
305  }
306
307  private void convertToR3(InputStream source, OutputStream dest, FhirFormat format) throws FHIRException, IOException {
308    org.hl7.fhir.dstu3.elementmodel.Element r2 = new org.hl7.fhir.dstu3.elementmodel.XmlParser(contextR2).parse(source);
309    StructureMap map = library.get("http://hl7.org/fhir/StructureMap/" + r2.fhirType() + "2to3");
310    if (map == null)
311      throw new FHIRException("No Map Found from R2 to R3 for " + r2.fhirType());
312    String tn = smu3.getTargetType(map).getType();
313    Resource r3 = ResourceFactory.createResource(tn);
314    smu3.transform(new TransformContextR2R3(contextR3, r2.getChildValue("id")), r2, map, r3);
315    FormatUtilities.makeParser(format).setOutputStyle(style).compose(dest, r3);
316  }
317
318  private void convertToR2(InputStream source, OutputStream dest, FhirFormat format) throws FHIRException, IOException {
319    org.hl7.fhir.dstu3.elementmodel.Element r3 = new org.hl7.fhir.dstu3.elementmodel.XmlParser(contextR3).parse(source);
320    StructureMap map = library.get("??");
321    String tn = smu3.getTargetType(map).getType();
322    StructureDefinition sd = smu2.getTargetType(map);
323    org.hl7.fhir.dstu3.elementmodel.Element r2 = Manager.build(contextR2, sd);
324    smu2.transform(contextR2, r3, map, r2);
325    org.hl7.fhir.dstu3.elementmodel.Manager.compose(contextR2, r2, dest, format, style, null);
326  }
327
328  @Override
329  public void log(String message) {
330//    System.out.println(message);
331  }
332
333  @Override
334  public Base createType(Object appInfo, String name) throws FHIRException {
335    SimpleWorkerContext context = ((TransformContextR2R3) appInfo).getContext();
336    if (context == contextR2) {
337      StructureDefinition sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/DSTU2/StructureDefinition/" + name);
338      if (sd == null)
339        throw new FHIRException("Type not found: '" + name + "'");
340      return Manager.build(context, sd);
341    } else
342      return ResourceFactory.createResourceOrType(name);
343  }
344
345  @Override
346  public Base createResource(Object appInfo, Base res) {
347    if (res instanceof Resource && (res.fhirType().equals("CodeSystem") || res.fhirType().equals("CareTeam")) || res.fhirType().equals("PractitionerRole")) {
348      Resource r = (Resource) res;
349      extras.add(r);
350      r.setId(((TransformContextR2R3) appInfo).getId() + "-" + extras.size()); //todo: get this into appinfo
351    }
352    return res;
353  }
354
355  @Override
356  public Coding translate(Object appInfo, Coding source, String conceptMapUrl) throws FHIRException {
357    throw new Error("translate not done yet");
358  }
359
360  @Override
361  public Base resolveReference(Object appContext, String url) {
362    for (Resource r : extras) {
363      if (r instanceof MetadataResource) {
364        MetadataResource mr = (MetadataResource) r;
365        if (url.equals(mr.getUrl()))
366          return mr;
367      }
368      if (url.equals(r.fhirType() + "/" + r.getId()))
369        return r;
370    }
371
372    return null;
373  }
374
375  @Override
376  public List<Base> performSearch(Object appContext, String url) {
377    List<Base> results = new ArrayList<Base>();
378    String[] parts = url.split("\\?");
379    if (parts.length == 2 && parts[0].substring(1).equals("PractitionerRole")) {
380      String[] vals = parts[1].split("\\=");
381      if (vals.length == 2 && vals[0].equals("practitioner"))
382        for (Resource r : extras) {
383          if (r instanceof PractitionerRole && ((PractitionerRole) r).getPractitioner().getReference().equals("Practitioner/" + vals[1])) {
384            results.add(r);
385          }
386        }
387    }
388    return results;
389  }
390}