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