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