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}