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.ValidationEngineBuilder().fromSource(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   * Set up the validator with a terminology service
168   *
169   * @param txServer - the URL of the terminology service (http://tx.fhir.org/r4 default)
170   * @throws Exception
171   */
172  public void connectToTxSvc(String txServer, String log, String txCache) throws Exception {
173    validator.connectToTSServer(txServer, log, txCache, FhirPublication.R5);
174  }
175
176  /**
177   * get back a JSON object with information about the process.
178   * @return
179   */
180  public String status() {
181    JsonObject json = new JsonObject();
182    json.addProperty("custom-resource-count", resourceCount);
183    validator.getContext().reportStatus(json);
184    json.addProperty("validation-count", validationCount);
185    json.addProperty("convert-count", convertCount);
186    json.addProperty("unconvert-count", unConvertCount);
187    json.addProperty("exception-count", exceptionCount);
188    synchronized (lock) {
189      json.addProperty("last-exception", lastException);      
190    }
191
192    json.addProperty("mem-max", Runtime.getRuntime().maxMemory() / (1024*1024));
193    json.addProperty("mem-total", Runtime.getRuntime().totalMemory() / (1024*1024));
194    json.addProperty("mem-free", Runtime.getRuntime().freeMemory() / (1024*1024));
195    json.addProperty("mem-used", (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024*1024));
196
197    Gson gson = new GsonBuilder().create();
198    return gson.toJson(json);
199  }
200
201  /**
202   * Call when the host process encounters one of the following:
203   *  - (for validation):
204   *    - profile
205   *    - extension definition
206   *    - value set
207   *    - code system
208   * 
209   *  - (for conversion):
210   *    - structure map 
211   *    - concept map
212   *  
213   * @param source
214   * @throws Exception
215   */
216  public void seeResource(byte[] source, FhirFormat fmt) throws Exception {
217    try {
218      Resource r;
219      if (fmt == FhirFormat.JSON) {
220        r = new JsonParser().parse(source);
221      } else if (fmt == FhirFormat.JSON) {
222        r = new XmlParser().parse(source);
223      } else {
224        throw new Exception("Unsupported format "+fmt.name());
225      }
226      validator.seeResource(r);
227      resourceCount++;
228    } catch (Exception e) {
229      exceptionCount++;
230
231      synchronized (lock) {
232        lastException = e.getMessage();
233      }
234      throw e;
235    }
236  }
237
238  /**
239   * forget a resource that was previously seen (using @seeResource)
240   * 
241   * @param type - the resource type
242   * @param id - the resource id 
243   * 
244   * @throws Exception
245   */
246  public void dropResource(String type, String id) throws Exception  {
247    try {
248      validator.dropResource(type, id);
249      resourceCount--;
250    } catch (Exception e) {
251      exceptionCount++;
252      synchronized (lock) {
253        lastException = e.getMessage();
254      }
255      throw e;
256    }
257  }
258
259  /**
260   * Validate a resource. 
261   * 
262   * Possible options:
263   *   - id-optional : no resource id is required (default) 
264   *   - id-required : a resource id is required
265   *   - id-prohibited : no resource id is allowed
266   *   - any-extensions : allow extensions other than those defined by the encountered structure definitions
267   *   - bp-ignore : ignore best practice recommendations (default)
268   *   - bp-hint : treat best practice recommendations as a hint
269   *   - bp-warning : treat best practice recommendations as a warning 
270   *   - bp-error : treat best practice recommendations as an error
271   *   - display-ignore : ignore Coding.display and do not validate it (default)
272   *   - display-check : check Coding.display - must be correct
273   *   - display-case-space : check Coding.display but allow case and whitespace variation
274   *   - display-case : check Coding.display but allow case variation
275   *   - display-space : check Coding.display but allow whitespace variation
276   *    
277   * @param location - a text description of the context of validation (for human consumers to help locate the problem - echoed into error messages)
278   * @param source - the bytes to validate
279   * @param cntType - the format of the content. one of XML, JSON, TURTLE
280   * @param options - a list of space separated options 
281   * @return
282   * @throws Exception
283   */
284  public byte[] validateResource(String location, byte[] source, String cntType, String options) throws Exception {
285    try {
286      IdStatus resourceIdRule = IdStatus.OPTIONAL;
287      boolean anyExtensionsAllowed = true;
288      BestPracticeWarningLevel bpWarnings = BestPracticeWarningLevel.Ignore;
289      CheckDisplayOption displayOption = CheckDisplayOption.Ignore;
290      for (String s : options.split(" ")) {
291        if ("id-optional".equalsIgnoreCase(s))
292          resourceIdRule = IdStatus.OPTIONAL;
293        else if ("id-required".equalsIgnoreCase(s))
294          resourceIdRule = IdStatus.REQUIRED;
295        else if ("id-prohibited".equalsIgnoreCase(s))
296          resourceIdRule = IdStatus.PROHIBITED;
297        else if ("any-extensions".equalsIgnoreCase(s))
298          anyExtensionsAllowed = true; // This is already the default
299        else if ("strict-extensions".equalsIgnoreCase(s))
300          anyExtensionsAllowed = false;
301        else if ("bp-ignore".equalsIgnoreCase(s))
302          bpWarnings = BestPracticeWarningLevel.Ignore;
303        else if ("bp-hint".equalsIgnoreCase(s))
304          bpWarnings = BestPracticeWarningLevel.Hint;
305        else if ("bp-warning".equalsIgnoreCase(s))
306          bpWarnings = BestPracticeWarningLevel.Warning;
307        else if ("bp-error".equalsIgnoreCase(s))
308          bpWarnings = BestPracticeWarningLevel.Error;
309        else if ("display-ignore".equalsIgnoreCase(s))
310          displayOption = CheckDisplayOption.Ignore;
311        else if ("display-check".equalsIgnoreCase(s))
312          displayOption = CheckDisplayOption.Check;
313        else if ("display-case-space".equalsIgnoreCase(s))
314          displayOption = CheckDisplayOption.CheckCaseAndSpace;
315        else if ("display-case".equalsIgnoreCase(s))
316          displayOption = CheckDisplayOption.CheckCase;
317        else if ("display-space".equalsIgnoreCase(s))
318          displayOption = CheckDisplayOption.CheckSpace;
319        else if (!Utilities.noString(s))
320          throw new Exception("Unknown option "+s);
321      }
322
323      OperationOutcome oo = validator.validate(location, source, FhirFormat.valueOf(cntType), null, resourceIdRule, anyExtensionsAllowed, bpWarnings, displayOption);
324      ByteArrayOutputStream bs = new ByteArrayOutputStream();
325      new XmlParser().compose(bs, oo);
326      validationCount++;
327      return bs.toByteArray();
328    } catch (Exception e) {
329      exceptionCount++;
330      synchronized (lock) {
331        lastException = e.getMessage();
332      }
333      throw e;
334    }
335  }
336
337  /**
338   * Convert a resource to R4 from the specified version
339   * 
340   * @param r - the source of the resource to convert from
341   * @param fmt  - the format of the content. one of XML, JSON, TURTLE
342   * @param version - the version of the content. one of r2, r3
343   * @return - the converted resource (or an exception if can't be converted)
344   * @throws FHIRException
345   * @throws IOException
346   */
347  public byte[] convertResource(byte[] r, String fmt, String version) throws FHIRException, IOException  {
348    try {
349      if (VersionUtilities.isR3Ver(version)) {
350        org.hl7.fhir.dstu3.formats.ParserBase p3 = org.hl7.fhir.dstu3.formats.FormatUtilities.makeParser(fmt);
351        org.hl7.fhir.dstu3.model.Resource res3 = p3.parse(r);
352        Resource res4 = VersionConvertorFactory_30_50.convertResource(res3);
353        org.hl7.fhir.r5.formats.ParserBase p4 = org.hl7.fhir.r5.formats.FormatUtilities.makeParser(fmt);
354        convertCount++;
355        return p4.composeBytes(res4);
356      } else if (VersionUtilities.isR2Ver(version)) {
357        org.hl7.fhir.dstu2.formats.ParserBase p2 = org.hl7.fhir.dstu2.formats.FormatUtilities.makeParser(fmt);
358        org.hl7.fhir.dstu2.model.Resource res2 = p2.parse(r);
359        Resource res4 = VersionConvertorFactory_10_50.convertResource(res2, conv_10_50_advisor);
360        org.hl7.fhir.r5.formats.ParserBase p4 = org.hl7.fhir.r5.formats.FormatUtilities.makeParser(fmt);
361        convertCount++;
362        return p4.composeBytes(res4);
363      } else if (VersionUtilities.isR2BVer(version)) {
364        org.hl7.fhir.dstu2016may.formats.ParserBase p2 = org.hl7.fhir.dstu2016may.formats.FormatUtilities.makeParser(fmt);
365        org.hl7.fhir.dstu2016may.model.Resource res2 = p2.parse(r);
366        Resource res4 = VersionConvertorFactory_14_50.convertResource(res2);
367        org.hl7.fhir.r5.formats.ParserBase p4 = org.hl7.fhir.r5.formats.FormatUtilities.makeParser(fmt);
368        convertCount++;
369        return p4.composeBytes(res4);
370      } else if (VersionUtilities.isR4Ver(version)) {
371        org.hl7.fhir.r4.formats.ParserBase p2 = org.hl7.fhir.r4.formats.FormatUtilities.makeParser(fmt);
372        org.hl7.fhir.r4.model.Resource res2 = p2.parse(r);
373        Resource res4 = VersionConvertorFactory_40_50.convertResource(res2);
374        org.hl7.fhir.r5.formats.ParserBase p4 = org.hl7.fhir.r5.formats.FormatUtilities.makeParser(fmt);
375        convertCount++;
376        return p4.composeBytes(res4);
377      } else
378        throw new FHIRException("Unsupported version "+version);
379    } catch (Exception e) {
380      exceptionCount++;
381      synchronized (lock) {
382        lastException = e.getMessage();
383      }
384      throw e;
385    }
386  }
387
388  /**
389   * Convert a resource from R4 to the specified version
390   * 
391   * @param r - the source of the resource to convert from
392   * @param fmt  - the format of the content. one of XML, JSON, TURTLE
393   * @param version - the version to convert to. one of r2, r3
394   * @return - the converted resource (or an exception if can't be converted)
395   * @throws FHIRException
396   * @throws IOException
397   */
398  public byte[] unConvertResource(byte[] r, String fmt, String version) throws FHIRException, IOException  {
399    try {
400      if ("3.0".equals(version) || "3.0.1".equals(version) || "r3".equals(version)) {
401        org.hl7.fhir.r5.formats.ParserBase p4 = org.hl7.fhir.r5.formats.FormatUtilities.makeParser(fmt);
402        org.hl7.fhir.r5.model.Resource res4 = p4.parse(r);
403        org.hl7.fhir.dstu3.model.Resource res3 = VersionConvertorFactory_30_50.convertResource(res4);
404        org.hl7.fhir.dstu3.formats.ParserBase p3 = org.hl7.fhir.dstu3.formats.FormatUtilities.makeParser(fmt);
405        unConvertCount++;
406        return p3.composeBytes(res3);
407      } else if ("1.0".equals(version) || "1.0.2".equals(version) || "r2".equals(version)) {
408        org.hl7.fhir.r5.formats.ParserBase p4 = org.hl7.fhir.r5.formats.FormatUtilities.makeParser(fmt);
409        org.hl7.fhir.r5.model.Resource res4 = p4.parse(r);
410        org.hl7.fhir.dstu2.model.Resource res2 = VersionConvertorFactory_10_50.convertResource(res4, conv_10_50_advisor);
411        org.hl7.fhir.dstu2.formats.ParserBase p2 = org.hl7.fhir.dstu2.formats.FormatUtilities.makeParser(fmt);
412        unConvertCount++;
413        return p2.composeBytes(res2);
414      } else if ("1.4".equals(version) || "1.4.0".equals(version)) {
415        org.hl7.fhir.r5.formats.ParserBase p4 = org.hl7.fhir.r5.formats.FormatUtilities.makeParser(fmt);
416        org.hl7.fhir.r5.model.Resource res4 = p4.parse(r);
417        org.hl7.fhir.dstu2016may.model.Resource res2 = VersionConvertorFactory_14_50.convertResource(res4);
418        org.hl7.fhir.dstu2016may.formats.ParserBase p2 = org.hl7.fhir.dstu2016may.formats.FormatUtilities.makeParser(fmt);
419        unConvertCount++;
420        return p2.composeBytes(res2);
421      } else
422        throw new FHIRException("Unsupported version "+version);
423    } catch (Exception e) {
424      exceptionCount++;
425      synchronized (lock) {
426        lastException = e.getMessage();
427      }
428      throw e;
429    }
430  }
431
432
433}