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}