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