001package org.hl7.fhir.validation;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006  
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009    
010   * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012   * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015   * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018  
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029  
030 */
031
032
033
034import java.io.ByteArrayOutputStream;
035import java.io.IOException;
036
037import org.hl7.fhir.convertors.factory.VersionConvertorFactory_10_50;
038import org.hl7.fhir.convertors.factory.VersionConvertorFactory_14_50;
039import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_50;
040import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50;
041import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_10_50;
042import org.hl7.fhir.exceptions.FHIRException;
043
044/**
045 * This class wraps up the validation and conversion infrastructure
046 * so it can be hosted inside a native server
047 * 
048 * workflow is pretty simple:
049 *  - create a DelphiLibraryHost, provide with path to library and tx server to use
050 *    (tx server is usually the host server)
051 *  - any structure definitions, value sets, code systems changes on the server get sent to tp seeResource or dropResource
052 *  - server wants to validate a resource, it calls validateResource and gets an operation outcome back
053 *  - server wants to convert from R4 to something else, it calls convertResource  
054 *  - server wants to convert to R4 from something else, it calls unConvertResource  
055 *  
056 * threading: todo: this class should be thread safe
057 *  
058 * note: this is a solution that uses lots of RAM...  
059 */
060
061import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat;
062import org.hl7.fhir.r5.formats.JsonParser;
063import org.hl7.fhir.r5.formats.XmlParser;
064import org.hl7.fhir.r5.model.CodeSystem;
065import org.hl7.fhir.r5.model.FhirPublication;
066import org.hl7.fhir.r5.model.OperationOutcome;
067import org.hl7.fhir.r5.model.Resource;
068import org.hl7.fhir.r5.model.ValueSet;
069import org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel;
070import org.hl7.fhir.r5.utils.validation.constants.CheckDisplayOption;
071import org.hl7.fhir.r5.utils.validation.constants.IdStatus;
072import org.hl7.fhir.utilities.Utilities;
073import org.hl7.fhir.utilities.VersionUtilities;
074
075import com.google.gson.Gson;
076import com.google.gson.GsonBuilder;
077import com.google.gson.JsonObject;
078import javax.annotation.Nonnull;
079
080/**
081 * This class allows you to host the java validator in another service, and use the services it has in a wider context. The way it works is 
082
083- put the jar in your class path
084- Find the class org.hl7.fhir.validation.NativeHostServices 
085- call init(path) where path refers to one of the definitions files from the main build (e.g. definitions.xml.zip) - required, do only once, do before anything else
086- call load(path) where path refers to the igpack.zip produced by the ig publisher (do this once for each IG you care about)
087- call connectToTxSvc(url) where the url is your terminology service of choice (can be http://tx.fhir.org/r4 or /r3)
088
089now the jar is ready for action. There's 3 functions you can call (all are thread safe):
090- validate - given a resource, validate it against all known rules
091- convert - given a resource in a different version convert it to this version (if possible)
092- unconvert - given a resource, convert it to a different version (if possible)
093
094
095also, call "status" to get a json object that describes the internals of the jar (e.g. for server status)
096
097
098The interface is optimised for JNI. 
099 * @author Grahame Grieve
100 *
101 */
102public class NativeHostServices {
103  
104  private class NH_10_50_Advisor extends BaseAdvisor_10_50 {
105    @Override
106    public void handleCodeSystem(@Nonnull CodeSystem tgtcs, @Nonnull ValueSet source) throws FHIRException {}
107
108    @Override
109    public CodeSystem getCodeSystem(@Nonnull ValueSet src) throws FHIRException {
110      throw new FHIRException("Code systems cannot be handled at this time"); // what to do? need thread local storage? 
111    }
112  }
113
114  private ValidationEngine validator;
115  private IgLoader igLoader;
116  private int validationCount = 0;
117  private int resourceCount = 0;
118  private int convertCount = 0;
119  private int unConvertCount = 0;
120  private int exceptionCount = 0;
121  private String lastException = null;  
122  private Object lock = new Object();
123
124  private final BaseAdvisor_10_50 conv_10_50_advisor = new NH_10_50_Advisor();
125
126  /**
127   * Create an instance of the service
128   */
129  public NativeHostServices()  {
130    super();
131  } 
132
133  /**
134   * Initialize the service and prepare it for use
135   * 
136   * @param pack - the filename of a pack from the main build - either definitions.xml.zip, definitions.json.zip, or igpack.zip 
137   * @throws Exception
138   */
139  public void init(String pack) throws Exception {
140    validator = new ValidationEngine(pack);
141    validator.getContext().setAllowLoadingDuplicates(true);
142    igLoader = new IgLoader(validator.getPcm(), validator.getContext(), validator.getVersion(), validator.isDebug());
143  }
144
145  /** 
146   * Load an IG so that the validator knows all about it.
147   * 
148   * @param pack - the filename (or URL) of a validator.pack produced by the IGPublisher
149   * 
150   * @throws Exception
151   */
152  public void load(String pack) throws Exception {
153    igLoader.loadIg(validator.getIgs(), validator.getBinaries(), pack, false);
154  }
155
156  /** 
157   * Set up the validator with a terminology service 
158   * 
159   * @param txServer - the URL of the terminology service (http://tx.fhir.org/r4 default)
160   * @throws Exception
161   */
162  public void connectToTxSvc(String txServer, String log) throws Exception {
163    validator.connectToTSServer(txServer, log, FhirPublication.R5);
164  }
165
166  /**
167   * get back a JSON object with information about the process.
168   * @return
169   */
170  public String status() {
171    JsonObject json = new JsonObject();
172    json.addProperty("custom-resource-count", resourceCount);
173    validator.getContext().reportStatus(json);
174    json.addProperty("validation-count", validationCount);
175    json.addProperty("convert-count", convertCount);
176    json.addProperty("unconvert-count", unConvertCount);
177    json.addProperty("exception-count", exceptionCount);
178    synchronized (lock) {
179      json.addProperty("last-exception", lastException);      
180    }
181
182    json.addProperty("mem-max", Runtime.getRuntime().maxMemory() / (1024*1024));
183    json.addProperty("mem-total", Runtime.getRuntime().totalMemory() / (1024*1024));
184    json.addProperty("mem-free", Runtime.getRuntime().freeMemory() / (1024*1024));
185    json.addProperty("mem-used", (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024*1024));
186
187    Gson gson = new GsonBuilder().create();
188    return gson.toJson(json);
189  }
190
191  /**
192   * Call when the host process encounters one of the following:
193   *  - (for validation):
194   *    - profile
195   *    - extension definition
196   *    - value set
197   *    - code system
198   * 
199   *  - (for conversion):
200   *    - structure map 
201   *    - concept map
202   *  
203   * @param source
204   * @throws Exception
205   */
206  public void seeResource(byte[] source, FhirFormat fmt) throws Exception {
207    try {
208      Resource r;
209      if (fmt == FhirFormat.JSON) {
210        r = new JsonParser().parse(source);
211      } else if (fmt == FhirFormat.JSON) {
212        r = new XmlParser().parse(source);
213      } else {
214        throw new Exception("Unsupported format "+fmt.name());
215      }
216      validator.seeResource(r);
217      resourceCount++;
218    } catch (Exception e) {
219      exceptionCount++;
220
221      synchronized (lock) {
222        lastException = e.getMessage();
223      }
224      throw e;
225    }
226  }
227
228  /**
229   * forget a resource that was previously seen (using @seeResource)
230   * 
231   * @param type - the resource type
232   * @param id - the resource id 
233   * 
234   * @throws Exception
235   */
236  public void dropResource(String type, String id) throws Exception  {
237    try {
238      validator.dropResource(type, id);
239      resourceCount--;
240    } catch (Exception e) {
241      exceptionCount++;
242      synchronized (lock) {
243        lastException = e.getMessage();
244      }
245      throw e;
246    }
247  }
248
249  /**
250   * Validate a resource. 
251   * 
252   * Possible options:
253   *   - id-optional : no resource id is required (default) 
254   *   - id-required : a resource id is required
255   *   - id-prohibited : no resource id is allowed
256   *   - any-extensions : allow extensions other than those defined by the encountered structure definitions
257   *   - bp-ignore : ignore best practice recommendations (default)
258   *   - bp-hint : treat best practice recommendations as a hint
259   *   - bp-warning : treat best practice recommendations as a warning 
260   *   - bp-error : treat best practice recommendations as an error
261   *   - display-ignore : ignore Coding.display and do not validate it (default)
262   *   - display-check : check Coding.display - must be correct
263   *   - display-case-space : check Coding.display but allow case and whitespace variation
264   *   - display-case : check Coding.display but allow case variation
265   *   - display-space : check Coding.display but allow whitespace variation
266   *    
267   * @param location - a text description of the context of validation (for human consumers to help locate the problem - echoed into error messages)
268   * @param source - the bytes to validate
269   * @param cntType - the format of the content. one of XML, JSON, TURTLE
270   * @param options - a list of space separated options 
271   * @return
272   * @throws Exception
273   */
274  public byte[] validateResource(String location, byte[] source, String cntType, String options) throws Exception {
275    try {
276      IdStatus resourceIdRule = IdStatus.OPTIONAL;
277      boolean anyExtensionsAllowed = true;
278      BestPracticeWarningLevel bpWarnings = BestPracticeWarningLevel.Ignore;
279      CheckDisplayOption displayOption = CheckDisplayOption.Ignore;
280      for (String s : options.split(" ")) {
281        if ("id-optional".equalsIgnoreCase(s))
282          resourceIdRule = IdStatus.OPTIONAL;
283        else if ("id-required".equalsIgnoreCase(s))
284          resourceIdRule = IdStatus.REQUIRED;
285        else if ("id-prohibited".equalsIgnoreCase(s))
286          resourceIdRule = IdStatus.PROHIBITED;
287        else if ("any-extensions".equalsIgnoreCase(s))
288          anyExtensionsAllowed = true; // This is already the default
289        else if ("strict-extensions".equalsIgnoreCase(s))
290          anyExtensionsAllowed = false;
291        else if ("bp-ignore".equalsIgnoreCase(s))
292          bpWarnings = BestPracticeWarningLevel.Ignore;
293        else if ("bp-hint".equalsIgnoreCase(s))
294          bpWarnings = BestPracticeWarningLevel.Hint;
295        else if ("bp-warning".equalsIgnoreCase(s))
296          bpWarnings = BestPracticeWarningLevel.Warning;
297        else if ("bp-error".equalsIgnoreCase(s))
298          bpWarnings = BestPracticeWarningLevel.Error;
299        else if ("display-ignore".equalsIgnoreCase(s))
300          displayOption = CheckDisplayOption.Ignore;
301        else if ("display-check".equalsIgnoreCase(s))
302          displayOption = CheckDisplayOption.Check;
303        else if ("display-case-space".equalsIgnoreCase(s))
304          displayOption = CheckDisplayOption.CheckCaseAndSpace;
305        else if ("display-case".equalsIgnoreCase(s))
306          displayOption = CheckDisplayOption.CheckCase;
307        else if ("display-space".equalsIgnoreCase(s))
308          displayOption = CheckDisplayOption.CheckSpace;
309        else if (!Utilities.noString(s))
310          throw new Exception("Unknown option "+s);
311      }
312
313      OperationOutcome oo = validator.validate(location, source, FhirFormat.valueOf(cntType), null, resourceIdRule, anyExtensionsAllowed, bpWarnings, displayOption);
314      ByteArrayOutputStream bs = new ByteArrayOutputStream();
315      new XmlParser().compose(bs, oo);
316      validationCount++;
317      return bs.toByteArray();
318    } catch (Exception e) {
319      exceptionCount++;
320      synchronized (lock) {
321        lastException = e.getMessage();
322      }
323      throw e;
324    }
325  }
326
327  /**
328   * Convert a resource to R4 from the specified version
329   * 
330   * @param r - the source of the resource to convert from
331   * @param fmt  - the format of the content. one of XML, JSON, TURTLE
332   * @param version - the version of the content. one of r2, r3
333   * @return - the converted resource (or an exception if can't be converted)
334   * @throws FHIRException
335   * @throws IOException
336   */
337  public byte[] convertResource(byte[] r, String fmt, String version) throws FHIRException, IOException  {
338    try {
339      if (VersionUtilities.isR3Ver(version)) {
340        org.hl7.fhir.dstu3.formats.ParserBase p3 = org.hl7.fhir.dstu3.formats.FormatUtilities.makeParser(fmt);
341        org.hl7.fhir.dstu3.model.Resource res3 = p3.parse(r);
342        Resource res4 = VersionConvertorFactory_30_50.convertResource(res3);
343        org.hl7.fhir.r5.formats.ParserBase p4 = org.hl7.fhir.r5.formats.FormatUtilities.makeParser(fmt);
344        convertCount++;
345        return p4.composeBytes(res4);
346      } else if (VersionUtilities.isR2Ver(version)) {
347        org.hl7.fhir.dstu2.formats.ParserBase p2 = org.hl7.fhir.dstu2.formats.FormatUtilities.makeParser(fmt);
348        org.hl7.fhir.dstu2.model.Resource res2 = p2.parse(r);
349        Resource res4 = VersionConvertorFactory_10_50.convertResource(res2, conv_10_50_advisor);
350        org.hl7.fhir.r5.formats.ParserBase p4 = org.hl7.fhir.r5.formats.FormatUtilities.makeParser(fmt);
351        convertCount++;
352        return p4.composeBytes(res4);
353      } else if (VersionUtilities.isR2BVer(version)) {
354        org.hl7.fhir.dstu2016may.formats.ParserBase p2 = org.hl7.fhir.dstu2016may.formats.FormatUtilities.makeParser(fmt);
355        org.hl7.fhir.dstu2016may.model.Resource res2 = p2.parse(r);
356        Resource res4 = VersionConvertorFactory_14_50.convertResource(res2);
357        org.hl7.fhir.r5.formats.ParserBase p4 = org.hl7.fhir.r5.formats.FormatUtilities.makeParser(fmt);
358        convertCount++;
359        return p4.composeBytes(res4);
360      } else if (VersionUtilities.isR4Ver(version)) {
361        org.hl7.fhir.r4.formats.ParserBase p2 = org.hl7.fhir.r4.formats.FormatUtilities.makeParser(fmt);
362        org.hl7.fhir.r4.model.Resource res2 = p2.parse(r);
363        Resource res4 = VersionConvertorFactory_40_50.convertResource(res2);
364        org.hl7.fhir.r5.formats.ParserBase p4 = org.hl7.fhir.r5.formats.FormatUtilities.makeParser(fmt);
365        convertCount++;
366        return p4.composeBytes(res4);
367      } else
368        throw new FHIRException("Unsupported version "+version);
369    } catch (Exception e) {
370      exceptionCount++;
371      synchronized (lock) {
372        lastException = e.getMessage();
373      }
374      throw e;
375    }
376  }
377
378  /**
379   * Convert a resource from R4 to the specified version
380   * 
381   * @param r - the source of the resource to convert from
382   * @param fmt  - the format of the content. one of XML, JSON, TURTLE
383   * @param version - the version to convert to. one of r2, r3
384   * @return - the converted resource (or an exception if can't be converted)
385   * @throws FHIRException
386   * @throws IOException
387   */
388  public byte[] unConvertResource(byte[] r, String fmt, String version) throws FHIRException, IOException  {
389    try {
390      if ("3.0".equals(version) || "3.0.1".equals(version) || "r3".equals(version)) {
391        org.hl7.fhir.r5.formats.ParserBase p4 = org.hl7.fhir.r5.formats.FormatUtilities.makeParser(fmt);
392        org.hl7.fhir.r5.model.Resource res4 = p4.parse(r);
393        org.hl7.fhir.dstu3.model.Resource res3 = VersionConvertorFactory_30_50.convertResource(res4);
394        org.hl7.fhir.dstu3.formats.ParserBase p3 = org.hl7.fhir.dstu3.formats.FormatUtilities.makeParser(fmt);
395        unConvertCount++;
396        return p3.composeBytes(res3);
397      } else if ("1.0".equals(version) || "1.0.2".equals(version) || "r2".equals(version)) {
398        org.hl7.fhir.r5.formats.ParserBase p4 = org.hl7.fhir.r5.formats.FormatUtilities.makeParser(fmt);
399        org.hl7.fhir.r5.model.Resource res4 = p4.parse(r);
400        org.hl7.fhir.dstu2.model.Resource res2 = VersionConvertorFactory_10_50.convertResource(res4, conv_10_50_advisor);
401        org.hl7.fhir.dstu2.formats.ParserBase p2 = org.hl7.fhir.dstu2.formats.FormatUtilities.makeParser(fmt);
402        unConvertCount++;
403        return p2.composeBytes(res2);
404      } else if ("1.4".equals(version) || "1.4.0".equals(version)) {
405        org.hl7.fhir.r5.formats.ParserBase p4 = org.hl7.fhir.r5.formats.FormatUtilities.makeParser(fmt);
406        org.hl7.fhir.r5.model.Resource res4 = p4.parse(r);
407        org.hl7.fhir.dstu2016may.model.Resource res2 = VersionConvertorFactory_14_50.convertResource(res4);
408        org.hl7.fhir.dstu2016may.formats.ParserBase p2 = org.hl7.fhir.dstu2016may.formats.FormatUtilities.makeParser(fmt);
409        unConvertCount++;
410        return p2.composeBytes(res2);
411      } else
412        throw new FHIRException("Unsupported version "+version);
413    } catch (Exception e) {
414      exceptionCount++;
415      synchronized (lock) {
416        lastException = e.getMessage();
417      }
418      throw e;
419    }
420  }
421
422
423}