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}