001package org.hl7.fhir.validation;
002
003import java.io.File;
004import java.net.Authenticator;
005import java.net.PasswordAuthentication;
006import java.util.ArrayList;
007import java.util.List;
008import java.util.Locale;
009
010/*
011  Copyright (c) 2011+, HL7, Inc.
012  All rights reserved.
013  
014  Redistribution and use in source and binary forms, with or without modification, 
015  are permitted provided that the following conditions are met:
016    
017   * Redistributions of source code must retain the above copyright notice, this 
018     list of conditions and the following disclaimer.
019   * Redistributions in binary form must reproduce the above copyright notice, 
020     this list of conditions and the following disclaimer in the documentation 
021     and/or other materials provided with the distribution.
022   * Neither the name of HL7 nor the names of its contributors may be used to 
023     endorse or promote products derived from this software without specific 
024     prior written permission.
025  
026  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
027  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
028  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
029  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
030  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
031  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
032  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
033  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
034  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
035  POSSIBILITY OF SUCH DAMAGE.
036  
037 */
038
039/*
040Copyright (c) 2011+, HL7, Inc
041All rights reserved.
042
043Redistribution and use in source and binary forms, with or without modification,
044are permitted provided that the following conditions are met:
045
046 * Redistributions of source code must retain the above copyright notice, this
047   list of conditions and the following disclaimer.
048 * Redistributions in binary form must reproduce the above copyright notice,
049   this list of conditions and the following disclaimer in the documentation
050   and/or other materials provided with the distribution.
051 * Neither the name of HL7 nor the names of its contributors may be used to
052   endorse or promote products derived from this software without specific
053   prior written permission.
054
055THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
056ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
057WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
058IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
059INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
060NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
061PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
062WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
063ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
064POSSIBILITY OF SUCH DAMAGE.
065
066*/
067
068import org.hl7.fhir.r5.model.ImplementationGuide;
069import org.hl7.fhir.r5.model.StructureDefinition;
070import org.hl7.fhir.r5.terminologies.JurisdictionUtilities;
071import org.hl7.fhir.utilities.FileFormat;
072import org.hl7.fhir.utilities.TimeTracker;
073import org.hl7.fhir.utilities.Utilities;
074import org.hl7.fhir.utilities.VersionUtilities;
075import org.hl7.fhir.utilities.npm.CommonPackages;
076import org.hl7.fhir.validation.cli.model.CliContext;
077import org.hl7.fhir.validation.cli.services.ComparisonService;
078import org.hl7.fhir.validation.cli.services.ValidationService;
079import org.hl7.fhir.validation.cli.utils.Display;
080import org.hl7.fhir.validation.cli.utils.EngineMode;
081import org.hl7.fhir.validation.cli.utils.Params;
082import org.hl7.fhir.validation.testexecutor.TestExecutor;
083import org.hl7.fhir.validation.testexecutor.TestExecutorParams;
084
085/**
086 * A executable class that will validate one or more FHIR resources against
087 * the specification
088 * <p>
089 * todo: schema validation (w3c xml, json schema, shex?)
090 * <p>
091 * if you want to host validation inside a process, skip this class, and look at
092 * ValidationEngine
093 * <p>
094 * todo: find a home for this:
095 *
096 * @author Grahame
097 */
098public class ValidatorCli {
099
100  public static final String HTTP_PROXY_HOST = "http.proxyHost";
101  public static final String HTTP_PROXY_PORT = "http.proxyPort";
102
103  public static final String HTTPS_PROXY_HOST = "https.proxyHost";
104
105  public static final String HTTPS_PROXY_PORT = "https.proxyPort";
106  public static final String HTTP_PROXY_USER = "http.proxyUser";
107  public static final String HTTP_PROXY_PASS = "http.proxyPassword";
108  public static final String JAVA_DISABLED_TUNNELING_SCHEMES = "jdk.http.auth.tunneling.disabledSchemes";
109  public static final String JAVA_DISABLED_PROXY_SCHEMES = "jdk.http.auth.proxying.disabledSchemes";
110  public static final String JAVA_USE_SYSTEM_PROXIES = "java.net.useSystemProxies";
111
112  private static ValidationService validationService = new ValidationService();
113
114  public static void main(String[] args) throws Exception {
115    TimeTracker tt = new TimeTracker();
116    TimeTracker.Session tts = tt.start("Loading");
117
118    args = addAdditionalParamsForIpsParam(args);
119    setJavaSystemProxyParamsFromParams(args);
120
121    Display.displayVersion();
122    Display.displaySystemInfo();
123
124    CliContext cliContext = Params.loadCliContext(args);
125    FileFormat.checkCharsetAndWarnIfNotUTF8(System.out);
126
127    if (shouldDisplayHelpToUser(args)) {
128      Display.displayHelpDetails();
129    } else if (Params.hasParam(args, Params.COMPARE)) {
130      if (destinationDirectoryValid(Params.getParam(args, Params.DESTINATION))) {
131        doLeftRightComparison(args, cliContext, tt);
132      }
133    } else if (Params.hasParam(args, Params.TEST)) {
134      parseTestParamsAndExecute(args);
135    }
136    else {
137      Display.printCliArgumentsAndInfo(args);
138      doValidation(tt, tts, cliContext);
139    }
140  }
141
142  private static void setJavaSystemProxyParamsFromParams(String[] args) {
143
144    setJavaSystemProxyHostFromParams(args, Params.PROXY, HTTP_PROXY_HOST, HTTP_PROXY_PORT);
145    setJavaSystemProxyHostFromParams(args, Params.HTTPS_PROXY, HTTPS_PROXY_HOST, HTTPS_PROXY_PORT);
146
147    if (Params.hasParam(args, Params.PROXY_AUTH)) {
148      assert Params.getParam(args, Params.PROXY) != null : "Cannot set PROXY_AUTH without setting PROXY...";
149      assert Params.getParam(args, Params.PROXY_AUTH) != null : "PROXY_AUTH arg passed in was NULL...";
150      String[] p = Params.getParam(args, Params.PROXY_AUTH).split(":");
151      String authUser = p[0];
152      String authPass = p[1];
153
154      /*
155       * For authentication, use java.net.Authenticator to set proxy's configuration and set the system properties
156       * http.proxyUser and http.proxyPassword
157       */
158      Authenticator.setDefault(
159        new Authenticator() {
160          @Override
161          public PasswordAuthentication getPasswordAuthentication() {
162            return new PasswordAuthentication(authUser, authPass.toCharArray());
163          }
164        }
165      );
166
167      System.setProperty(HTTP_PROXY_USER, authUser);
168      System.setProperty(HTTP_PROXY_PASS, authPass);
169      System.setProperty(JAVA_USE_SYSTEM_PROXIES, "true");
170
171      /*
172       * For Java 1.8 and higher you must set
173       * -Djdk.http.auth.tunneling.disabledSchemes=
174       * to make proxies with Basic Authorization working with https along with Authenticator
175       */
176      System.setProperty(JAVA_DISABLED_TUNNELING_SCHEMES, "");
177      System.setProperty(JAVA_DISABLED_PROXY_SCHEMES, "");
178    }
179  }
180
181  private static void setJavaSystemProxyHostFromParams(String[] args, String proxyParam, String proxyHostProperty, String proxyPortProperty) {
182    if (Params.hasParam(args, proxyParam)) {
183      assert Params.getParam(args, proxyParam) != null : "PROXY arg passed in was NULL";
184      String[] p = Params.getParam(args, proxyParam).split(":");
185
186      System.setProperty(proxyHostProperty, p[0]);
187      System.setProperty(proxyPortProperty, p[1]);
188    }
189  }
190
191  protected static void parseTestParamsAndExecute(String[] args) {
192    final String testModuleParam = Params.getParam(args, Params.TEST_MODULES);
193    final String testClassnameFilter = Params.getParam(args, Params.TEST_NAME_FILTER);
194    final String testCasesDirectory = Params.getParam(args, Params.TEST);
195    final String txCacheDirectory = Params.getParam(args, Params.TERMINOLOGY_CACHE);
196    assert TestExecutorParams.isValidModuleParam(testModuleParam) : "Invalid test module param: " + testModuleParam;
197    final String[] moduleNamesArg = TestExecutorParams.parseModuleParam(testModuleParam);
198
199    assert TestExecutorParams.isValidClassnameFilterParam(testClassnameFilter) : "Invalid regex for test classname filter: " + testClassnameFilter;
200
201    new TestExecutor(moduleNamesArg).executeTests(testClassnameFilter, txCacheDirectory, testCasesDirectory);
202
203    System.exit(0);
204  }
205
206  private static String[] addAdditionalParamsForIpsParam(String[] args) {
207    // ips$branch --> -version 4.0 -ig hl7.fhir.uv.ips#current$connectathon-2 -profile http://hl7.org/fhir/uv/ips/StructureDefinition/Bundle-uv-ips
208    List<String> res = new ArrayList<>();
209    for (String a : args) {
210      if (a.equals("-ips")) {
211        res.add("-version");
212        res.add("4.0");
213        res.add("-ig");
214        res.add("hl7.fhir.uv.ips#current");
215        res.add("-profile");
216        res.add("http://hl7.org/fhir/uv/ips/StructureDefinition/Bundle-uv-ips");
217      } else if (a.startsWith("-ips$")) {
218        res.add("-version");
219        res.add("4.0");
220        res.add("-ig");
221        res.add("hl7.fhir.uv.ips#current$"+a.substring(5));
222        res.add("-profile");
223        res.add("http://hl7.org/fhir/uv/ips/StructureDefinition/Bundle-uv-ips");        
224      } else {
225        res.add(a);
226      }
227    }
228    String[] r = new String[res.size()];
229    for (int i = 0; i < res.size(); i++) {
230      r[i] = res.get(i);
231    }
232    return r;
233  }
234
235  private static boolean destinationDirectoryValid(String dest) {
236    if (dest == null) {
237      System.out.println("no -dest parameter provided");
238      return false;
239    } else if (!new File(dest).isDirectory()) {
240      System.out.println("Specified destination (-dest parameter) is not valid: \"" + dest + "\")");
241      return false;
242    } else {
243      System.out.println("Valid destination directory provided: \"" + dest + "\")");
244      return true;
245    }
246  }
247
248  private static boolean shouldDisplayHelpToUser(String[] args) {
249    return (args.length == 0
250      || Params.hasParam(args, Params.HELP)
251      || Params.hasParam(args, "?")
252      || Params.hasParam(args, "-?")
253      || Params.hasParam(args, "/?"));
254  }
255
256  private static void doLeftRightComparison(String[] args, CliContext cliContext, TimeTracker tt) throws Exception {
257    Display.printCliArgumentsAndInfo(args);
258    if (cliContext.getSv() == null) {
259      cliContext.setSv(validationService.determineVersion(cliContext));
260    }
261    String v = VersionUtilities.getCurrentVersion(cliContext.getSv());
262    String definitions = VersionUtilities.packageForVersion(v) + "#" + v;
263    ValidationEngine validator = validationService.initializeValidator(cliContext, definitions, tt);
264    validator.loadPackage(CommonPackages.ID_PUBPACK, null);
265    ComparisonService.doLeftRightComparison(args, Params.getParam(args, Params.DESTINATION), validator);
266  }
267
268  private static void doValidation(TimeTracker tt, TimeTracker.Session tts, CliContext cliContext) throws Exception {
269    if (cliContext.getSv() == null) {
270      cliContext.setSv(validationService.determineVersion(cliContext));
271    }
272    if (cliContext.getJurisdiction() == null) {
273      System.out.println("  Jurisdiction: None specified (locale = "+Locale.getDefault().getCountry()+")");      
274      System.out.println("  Note that exceptions and validation failures may happen in the absense of a locale");      
275    } else {
276      System.out.println("  Jurisdiction: "+JurisdictionUtilities.displayJurisdiction(cliContext.getJurisdiction()));
277    }
278
279    System.out.println("Loading");
280    // Comment this out because definitions filename doesn't necessarily contain version (and many not even be 14 characters long).
281    // Version gets spit out a couple of lines later after we've loaded the context
282    String definitions = "dev".equals(cliContext.getSv()) ? "hl7.fhir.r5.core#current" : VersionUtilities.packageForVersion(cliContext.getSv()) + "#" + VersionUtilities.getCurrentVersion(cliContext.getSv());
283    ValidationEngine validator = validationService.initializeValidator(cliContext, definitions, tt);
284    tts.end();
285    switch (cliContext.getMode()) {
286      case TRANSFORM:
287        validationService.transform(cliContext, validator);
288        break;
289      case COMPILE:
290        validationService.compile(cliContext, validator);
291        break;
292      case NARRATIVE:
293        validationService.generateNarrative(cliContext, validator);
294        break;
295      case SNAPSHOT:
296        validationService.generateSnapshot(cliContext, validator);
297        break;
298      case SPREADSHEET:
299        validationService.generateSpreadsheet(cliContext, validator);
300        break;
301      case CONVERT:
302        validationService.convertSources(cliContext, validator);
303        break;
304      case FHIRPATH:
305        validationService.evaluateFhirpath(cliContext, validator);
306        break;
307      case VERSION:
308        validationService.transformVersion(cliContext, validator);
309        break;
310      case VALIDATION:
311      case SCAN:
312      default:
313        for (String s : cliContext.getProfiles()) {
314          if (!validator.getContext().hasResource(StructureDefinition.class, s) && !validator.getContext().hasResource(ImplementationGuide.class, s)) {
315            System.out.println("  Fetch Profile from " + s);
316            validator.loadProfile(cliContext.getLocations().getOrDefault(s, s));
317          }
318        }
319        System.out.println("Validating");
320        if (cliContext.getMode() == EngineMode.SCAN) {
321          Scanner validationScanner = new Scanner(validator.getContext(), validator.getValidator(null), validator.getIgLoader(), validator.getFhirPathEngine());
322          validationScanner.validateScan(cliContext.getOutput(), cliContext.getSources());
323        } else {
324          validationService.validateSources(cliContext, validator);
325        }
326        break;
327    }
328    System.out.println("Done. " + tt.report()+". Max Memory = "+Utilities.describeSize(Runtime.getRuntime().maxMemory()));
329  }
330}