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.utilities.TextFile;
070
071/**
072 * This class manages conversion from R2 to R3 and vice versa
073 * <p>
074 * To use this class, do the following:
075 * <p>
076 * - provide a stream or path (file or URL) that points to R2 definitions (from http://hl7.org/fhir/DSTU2/downloads.html)
077 * - provide a stream or a path (file or URL) that points to the R3 definitions  (from http://hl7.org/fhir/STU3/downloads.html)
078 * - provide a stream or a path (file or URL) that points to R2/R3 map files (from http://hl7.org/fhir/r2r3maps.zip)
079 * <p>
080 * - call convert() (can call this more than once, but not multithread safe)
081 *
082 * @author Grahame Grieve
083 */
084public class R2R3ConversionManager implements ITransformerServices {
085
086  private final Map<String, StructureMap> library = new HashMap<String, StructureMap>();
087  private final List<Resource> extras = new ArrayList<Resource>();
088  private SimpleWorkerContext contextR2;
089  private SimpleWorkerContext contextR3;
090  private boolean needPrepare = false;
091  private StructureMapUtilities smu3;
092  private StructureMapUtilities smu2;
093  private OutputStyle style = OutputStyle.PRETTY;
094
095  public static void main(String[] args) throws IOException, FHIRException {
096    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"))) {
097      System.out.println("R2 <--> R3 Convertor");
098      System.out.println("====================");
099      System.out.println();
100      System.out.println("parameters: -d2 [r2 definitions] -d3 [r3 definitions] -maps [map source] -src [source] -dest [dest] -r2/3 - fmt [format]");
101      System.out.println();
102      System.out.println("d2: definitions from http://hl7.org/fhir/DSTU2/downloads.html");
103      System.out.println("d3: definitions from http://hl7.org/fhir/STU3/downloads.html");
104      System.out.println("maps: R2/R3 maps from http://hl7.org/fhir/r2r3maps.zip");
105      System.out.println("src: filename for source to convert");
106      System.out.println("dest: filename for destination of conversion");
107      System.out.println("-r2: source is r2, convert to r3");
108      System.out.println("-r3: source is r3, convert to r2");
109      System.out.println("-fmt: xml | json (xml is default)");
110    } else {
111      R2R3ConversionManager self = new R2R3ConversionManager();
112      self.setR2Definitions(getNamedParam(args, "-d2"));
113      self.setR3Definitions(getNamedParam(args, "-d3"));
114      self.setMappingLibrary(getNamedParam(args, "-maps"));
115      FhirFormat fmt = hasParam(args, "-fmt") ? getNamedParam(args, "-fmt").equalsIgnoreCase("json") ? FhirFormat.JSON : FhirFormat.XML : FhirFormat.XML;
116      InputStream src = new FileInputStream(getNamedParam(args, "-src"));
117      OutputStream dst = new FileOutputStream(getNamedParam(args, "-dest"));
118      self.convert(src, dst, hasParam(args, "-r2"), fmt);
119    }
120  }
121
122  private static boolean hasParam(String[] args, String param) {
123    for (String a : args)
124      if (a.equals(param))
125        return true;
126    return false;
127  }
128
129  private static String getNamedParam(String[] args, String param) {
130    boolean found = false;
131    for (String a : args) {
132      if (found)
133        return a;
134      if (a.equals(param)) {
135        found = true;
136      }
137    }
138    return null;
139  }
140
141  public OutputStyle getStyle() {
142    return style;
143  }
144
145  public void setStyle(OutputStyle style) {
146    this.style = style;
147  }
148
149  public List<Resource> getExtras() {
150    return extras;
151  }
152
153  // set up ------------------------------------------------------------------
154  public void setR2Definitions(InputStream stream) throws IOException, FHIRException {
155    needPrepare = true;
156    R2ToR3Loader ldr = new R2ToR3Loader();
157    ldr.setPatchUrls(true).setKillPrimitives(true);
158    Map<String, InputStream> files = readInputStream(stream);
159    contextR2 = new SimpleWorkerContext();
160    contextR2.setAllowLoadingDuplicates(true);
161    contextR2.loadFromFile(files.get("profiles-types.xml"), "profiles-types.xml", ldr);
162    contextR2.loadFromFile(files.get("profiles-resources.xml"), "profiles-resources.xml", ldr);
163    contextR2.loadFromFile(files.get("valuesets.xml"), "valuesets.xml", ldr);
164  }
165
166  public void setR2Definitions(String source) throws IOException, FHIRException {
167    File f = new File(source);
168    if (f.exists())
169      setR2Definitions(new FileInputStream(f));
170    else
171      setR2Definitions(fetch(source));
172  }
173
174  public void setR3Definitions(InputStream stream) throws IOException, FHIRException {
175    needPrepare = true;
176    Map<String, InputStream> files = readInputStream(stream);
177    contextR3 = new SimpleWorkerContext();
178    contextR2.setAllowLoadingDuplicates(true);
179    contextR3.loadFromFile(files.get("profiles-types.xml"), "profiles-types.xml", null);
180    contextR3.loadFromFile(files.get("profiles-resources.xml"), "profiles-resources.xml", null);
181    contextR3.loadFromFile(files.get("extension-definitions.xml"), "extension-definitions.xml", null);
182    contextR3.loadFromFile(files.get("valuesets.xml"), "valuesets.xml", null);
183    contextR3.setCanRunWithoutTerminology(true);
184  }
185
186  public void setR3Definitions(String source) throws IOException, FHIRException {
187    File f = new File(source);
188    if (f.exists())
189      setR3Definitions(new FileInputStream(f));
190    else
191      setR3Definitions(fetch(source));
192  }
193
194  public void setMappingLibrary(InputStream stream) throws IOException, FHIRException {
195    needPrepare = true;
196    Map<String, InputStream> files = readInputStream(stream);
197    for (InputStream s : files.values()) {
198      StructureMap sm = new StructureMapUtilities(contextR3).parse(TextFile.streamToString(s));
199      library.put(sm.getUrl(), sm);
200    }
201  }
202
203  public void setMappingLibrary(String source) throws IOException, FHIRException {
204    File f = new File(source);
205    if (f.exists())
206      setMappingLibrary(new FileInputStream(f));
207    else
208      setMappingLibrary(fetch(source));
209  }
210
211  // support
212  private InputStream fetch(String source) {
213    throw new Error("not done yet");
214  }
215
216  private Map<String, InputStream> readInputStream(InputStream stream) throws IOException {
217    Map<String, InputStream> res = new HashMap<String, InputStream>();
218    ZipInputStream zip = new ZipInputStream(stream);
219    ZipEntry ze = null;
220    while ((ze = zip.getNextEntry()) != null) {
221      String n = ze.getName();
222      ByteArrayOutputStream bs = new ByteArrayOutputStream();
223      for (int c = zip.read(); c != -1; c = zip.read()) {
224        bs.write(c);
225      }
226      bs.close();
227      res.put(n, new ByteArrayInputStream(bs.toByteArray()));
228      zip.closeEntry();
229    }
230    zip.close();
231    return res;
232  }
233
234  private void prepare() throws FHIRException {
235    if (contextR2 == null)
236      throw new FHIRException("No R2 definitions provided");
237    if (contextR3 == null)
238      throw new FHIRException("No R3 definitions provided");
239    if (library == null)
240      throw new FHIRException("No R2/R# conversion maps provided");
241
242    if (needPrepare) {
243      for (StructureDefinition sd : contextR2.allStructures()) {
244        StructureDefinition sdn = sd.copy();
245        sdn.getExtension().clear();
246        contextR3.seeResource(sdn.getUrl(), sdn);
247      }
248
249      for (StructureDefinition sd : contextR3.allStructures()) {
250        if (sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE) {
251          contextR2.seeResource(sd.getUrl(), sd);
252          StructureDefinition sdn = sd.copy();
253          sdn.setUrl(sdn.getUrl().replace("http://hl7.org/fhir/", "http://hl7.org/fhir/DSTU2/"));
254          sdn.addExtension().setUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace").setValue(new UriType("http://hl7.org/fhir"));
255          contextR2.seeResource(sdn.getUrl(), sdn);
256          contextR3.seeResource(sdn.getUrl(), sdn);
257        }
258      }
259
260      contextR2.setExpansionProfile(new ExpansionProfile().setUrl("urn:uuid:" + UUID.randomUUID().toString().toLowerCase()));
261      contextR3.setExpansionProfile(new ExpansionProfile().setUrl("urn:uuid:" + UUID.randomUUID().toString().toLowerCase()));
262
263      smu3 = new StructureMapUtilities(contextR3, library, this);
264      smu2 = new StructureMapUtilities(contextR2, library, this);
265
266      needPrepare = false;
267    }
268  }
269
270  // execution
271  public byte[] convert(byte[] source, boolean r2ToR3, FhirFormat format) throws FHIRException, IOException {
272    prepare();
273    ByteArrayOutputStream bs = new ByteArrayOutputStream();
274    if (r2ToR3)
275      convertToR3(new ByteArrayInputStream(source), bs, format);
276    else
277      convertToR2(new ByteArrayInputStream(source), bs, format);
278    bs.close();
279    return bs.toByteArray();
280  }
281
282  public void convert(InputStream source, OutputStream dest, boolean r2ToR3, FhirFormat format) throws FHIRException, IOException {
283    prepare();
284    if (r2ToR3)
285      convertToR3(source, dest, format);
286    else
287      convertToR2(source, dest, format);
288  }
289
290  public org.hl7.fhir.dstu2.model.Resource convert(org.hl7.fhir.dstu3.model.Resource source) throws IOException, FHIRException {
291    ByteArrayOutputStream bs = new ByteArrayOutputStream();
292    new JsonParser().compose(bs, source);
293    bs.close();
294    return new org.hl7.fhir.dstu2.formats.JsonParser().parse(convert(bs.toByteArray(), false, FhirFormat.JSON));
295  }
296
297  public org.hl7.fhir.dstu3.model.Resource convert(org.hl7.fhir.dstu2.model.Resource source) throws IOException, FHIRException {
298    ByteArrayOutputStream bs = new ByteArrayOutputStream();
299    new org.hl7.fhir.dstu2.formats.JsonParser().compose(bs, source);
300    bs.close();
301    return new JsonParser().parse(convert(bs.toByteArray(), false, FhirFormat.JSON));
302  }
303
304  private void convertToR3(InputStream source, OutputStream dest, FhirFormat format) throws FHIRException, IOException {
305    org.hl7.fhir.dstu3.elementmodel.Element r2 = new org.hl7.fhir.dstu3.elementmodel.XmlParser(contextR2).parse(source);
306    StructureMap map = library.get("http://hl7.org/fhir/StructureMap/" + r2.fhirType() + "2to3");
307    if (map == null)
308      throw new FHIRException("No Map Found from R2 to R3 for " + r2.fhirType());
309    String tn = smu3.getTargetType(map).getType();
310    Resource r3 = ResourceFactory.createResource(tn);
311    smu3.transform(new TransformContextR2R3(contextR3, r2.getChildValue("id")), r2, map, r3);
312    FormatUtilities.makeParser(format).setOutputStyle(style).compose(dest, r3);
313  }
314
315  private void convertToR2(InputStream source, OutputStream dest, FhirFormat format) throws FHIRException, IOException {
316    org.hl7.fhir.dstu3.elementmodel.Element r3 = new org.hl7.fhir.dstu3.elementmodel.XmlParser(contextR3).parse(source);
317    StructureMap map = library.get("??");
318    String tn = smu3.getTargetType(map).getType();
319    StructureDefinition sd = smu2.getTargetType(map);
320    org.hl7.fhir.dstu3.elementmodel.Element r2 = Manager.build(contextR2, sd);
321    smu2.transform(contextR2, r3, map, r2);
322    org.hl7.fhir.dstu3.elementmodel.Manager.compose(contextR2, r2, dest, format, style, null);
323  }
324
325  @Override
326  public void log(String message) {
327//    System.out.println(message);
328  }
329
330  @Override
331  public Base createType(Object appInfo, String name) throws FHIRException {
332    SimpleWorkerContext context = ((TransformContextR2R3) appInfo).getContext();
333    if (context == contextR2) {
334      StructureDefinition sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/DSTU2/StructureDefinition/" + name);
335      if (sd == null)
336        throw new FHIRException("Type not found: '" + name + "'");
337      return Manager.build(context, sd);
338    } else
339      return ResourceFactory.createResourceOrType(name);
340  }
341
342  @Override
343  public Base createResource(Object appInfo, Base res) {
344    if (res instanceof Resource && (res.fhirType().equals("CodeSystem") || res.fhirType().equals("CareTeam")) || res.fhirType().equals("PractitionerRole")) {
345      Resource r = (Resource) res;
346      extras.add(r);
347      r.setId(((TransformContextR2R3) appInfo).getId() + "-" + extras.size()); //todo: get this into appinfo
348    }
349    return res;
350  }
351
352  @Override
353  public Coding translate(Object appInfo, Coding source, String conceptMapUrl) throws FHIRException {
354    throw new Error("translate not done yet");
355  }
356
357  @Override
358  public Base resolveReference(Object appContext, String url) {
359    for (Resource r : extras) {
360      if (r instanceof MetadataResource) {
361        MetadataResource mr = (MetadataResource) r;
362        if (url.equals(mr.getUrl()))
363          return mr;
364      }
365      if (url.equals(r.fhirType() + "/" + r.getId()))
366        return r;
367    }
368
369    return null;
370  }
371
372  @Override
373  public List<Base> performSearch(Object appContext, String url) {
374    List<Base> results = new ArrayList<Base>();
375    String[] parts = url.split("\\?");
376    if (parts.length == 2 && parts[0].substring(1).equals("PractitionerRole")) {
377      String[] vals = parts[1].split("\\=");
378      if (vals.length == 2 && vals[0].equals("practitioner"))
379        for (Resource r : extras) {
380          if (r instanceof PractitionerRole && ((PractitionerRole) r).getPractitioner().getReference().equals("Practitioner/" + vals[1])) {
381            results.add(r);
382          }
383        }
384    }
385    return results;
386  }
387}