001package ca.uhn.fhir.validation.schematron; 002 003/* 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.rest.api.EncodingEnum; 025import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 026import ca.uhn.fhir.util.BundleUtil; 027import ca.uhn.fhir.validation.FhirValidator; 028import ca.uhn.fhir.validation.IValidationContext; 029import ca.uhn.fhir.validation.IValidatorModule; 030import ca.uhn.fhir.validation.ResultSeverityEnum; 031import ca.uhn.fhir.validation.SchemaBaseValidator; 032import ca.uhn.fhir.validation.SingleValidationMessage; 033import ca.uhn.fhir.validation.ValidationContext; 034import com.helger.commons.error.IError; 035import com.helger.commons.error.list.IErrorList; 036import com.helger.schematron.ISchematronResource; 037import com.helger.schematron.SchematronHelper; 038import com.helger.schematron.svrl.jaxb.SchematronOutputType; 039import com.helger.schematron.xslt.SchematronResourceSCH; 040import org.hl7.fhir.instance.model.api.IBaseBundle; 041import org.hl7.fhir.instance.model.api.IBaseResource; 042import org.slf4j.Logger; 043import org.slf4j.LoggerFactory; 044 045import javax.xml.transform.stream.StreamSource; 046import java.io.IOException; 047import java.io.InputStream; 048import java.io.StringReader; 049import java.util.HashMap; 050import java.util.List; 051import java.util.Locale; 052import java.util.Map; 053 054/** 055 * This class is only used using reflection from {@link SchematronProvider} in order 056 * to be truly optional. 057 */ 058public class SchematronBaseValidator implements IValidatorModule { 059 060 private static final Logger ourLog = LoggerFactory.getLogger(SchematronBaseValidator.class); 061 private final Map<Class<? extends IBaseResource>, ISchematronResource> myClassToSchematron = new HashMap<>(); 062 private FhirContext myCtx; 063 064 /** 065 * Constructor 066 */ 067 public SchematronBaseValidator(FhirContext theContext) { 068 myCtx = theContext; 069 } 070 071 @Override 072 public void validateResource(IValidationContext<IBaseResource> theCtx) { 073 074 if (theCtx.getResource() instanceof IBaseBundle) { 075 IBaseBundle bundle = (IBaseBundle) theCtx.getResource(); 076 List<IBaseResource> subResources = BundleUtil.toListOfResources(myCtx, bundle); 077 for (IBaseResource nextSubResource : subResources) { 078 validateResource(ValidationContext.subContext(theCtx, nextSubResource, theCtx.getOptions())); 079 } 080 } 081 082 ISchematronResource sch = getSchematron(theCtx); 083 String resourceAsString; 084 if (theCtx.getResourceAsStringEncoding() == EncodingEnum.XML) { 085 resourceAsString = theCtx.getResourceAsString(); 086 } else { 087 resourceAsString = theCtx.getFhirContext().newXmlParser().encodeResourceToString(theCtx.getResource()); 088 } 089 StreamSource source = new StreamSource(new StringReader(resourceAsString)); 090 091 SchematronOutputType results = SchematronHelper.applySchematron(sch, source); 092 if (results == null) { 093 return; 094 } 095 096 IErrorList errors = SchematronHelper.convertToErrorList(results, theCtx.getFhirContext().getResourceDefinition(theCtx.getResource()).getBaseDefinition().getName()); 097 098 if (errors.getAllErrors().containsOnlySuccess()) { 099 return; 100 } 101 102 for (IError next : errors) { 103 ResultSeverityEnum severity; 104 if (next.isFailure()) { 105 severity = ResultSeverityEnum.ERROR; 106 } else if (next.isError()) { 107 severity = ResultSeverityEnum.FATAL; 108 } else if (next.isNoError()) { 109 severity = ResultSeverityEnum.WARNING; 110 } else { 111 continue; 112 } 113 114 String details = next.getAsString(Locale.getDefault()); 115 116 SingleValidationMessage message = new SingleValidationMessage(); 117 message.setMessage(details); 118 message.setLocationLine(next.getErrorLocation().getLineNumber()); 119 message.setLocationCol(next.getErrorLocation().getColumnNumber()); 120 message.setLocationString(next.getErrorLocation().getAsString()); 121 message.setSeverity(severity); 122 theCtx.addValidationMessage(message); 123 } 124 125 } 126 127 private ISchematronResource getSchematron(IValidationContext<IBaseResource> theCtx) { 128 Class<? extends IBaseResource> resource = theCtx.getResource().getClass(); 129 Class<? extends IBaseResource> baseResourceClass = theCtx.getFhirContext().getResourceDefinition(resource).getBaseDefinition().getImplementingClass(); 130 131 return getSchematronAndCache(theCtx, baseResourceClass); 132 } 133 134 private ISchematronResource getSchematronAndCache(IValidationContext<IBaseResource> theCtx, Class<? extends IBaseResource> theClass) { 135 synchronized (myClassToSchematron) { 136 ISchematronResource retVal = myClassToSchematron.get(theClass); 137 if (retVal != null) { 138 return retVal; 139 } 140 141 String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theCtx.getFhirContext().getResourceDefinition(theCtx.getResource()).getBaseDefinition().getName().toLowerCase() 142 + ".sch"; 143 try (InputStream baseIs = FhirValidator.class.getResourceAsStream(pathToBase)) { 144 if (baseIs == null) { 145 throw new InternalErrorException("Failed to load schematron for resource '" + theCtx.getFhirContext().getResourceDefinition(theCtx.getResource()).getBaseDefinition().getName() + "'. " 146 + SchemaBaseValidator.RESOURCES_JAR_NOTE); 147 } 148 } catch (IOException e) { 149 ourLog.error("Failed to close stream", e); 150 } 151 152 retVal = SchematronResourceSCH.fromClassPath(pathToBase); 153 myClassToSchematron.put(theClass, retVal); 154 return retVal; 155 } 156 } 157}