001package ca.uhn.fhir.validation.schematron; 002 003/* 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2019 University Health Network 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.*; 028import com.helger.commons.error.IError; 029import com.helger.commons.error.list.IErrorList; 030import com.helger.schematron.ISchematronResource; 031import com.helger.schematron.SchematronHelper; 032import com.helger.schematron.xslt.SchematronResourceSCH; 033import org.hl7.fhir.instance.model.api.IBaseBundle; 034import org.hl7.fhir.instance.model.api.IBaseResource; 035import org.oclc.purl.dsdl.svrl.SchematronOutputType; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038 039import javax.xml.transform.stream.StreamSource; 040import java.io.IOException; 041import java.io.InputStream; 042import java.io.StringReader; 043import java.util.HashMap; 044import java.util.List; 045import java.util.Locale; 046import java.util.Map; 047 048/** 049 * This class is only used using reflection from {@link SchematronProvider} in order 050 * to be truly optional. 051 */ 052public class SchematronBaseValidator implements IValidatorModule { 053 054 private static final Logger ourLog = LoggerFactory.getLogger(SchematronBaseValidator.class); 055 private final Map<Class<? extends IBaseResource>, ISchematronResource> myClassToSchematron = new HashMap<>(); 056 private FhirContext myCtx; 057 058 /** 059 * Constructor 060 */ 061 public SchematronBaseValidator(FhirContext theContext) { 062 myCtx = theContext; 063 } 064 065 @Override 066 public void validateResource(IValidationContext<IBaseResource> theCtx) { 067 068 if (theCtx.getResource() instanceof IBaseBundle) { 069 IBaseBundle bundle = (IBaseBundle) theCtx.getResource(); 070 List<IBaseResource> subResources = BundleUtil.toListOfResources(myCtx, bundle); 071 for (IBaseResource nextSubResource : subResources) { 072 validateResource(ValidationContext.subContext(theCtx, nextSubResource, theCtx.getOptions())); 073 } 074 } 075 076 ISchematronResource sch = getSchematron(theCtx); 077 String resourceAsString; 078 if (theCtx.getResourceAsStringEncoding() == EncodingEnum.XML) { 079 resourceAsString = theCtx.getResourceAsString(); 080 } else { 081 resourceAsString = theCtx.getFhirContext().newXmlParser().encodeResourceToString(theCtx.getResource()); 082 } 083 StreamSource source = new StreamSource(new StringReader(resourceAsString)); 084 085 SchematronOutputType results = SchematronHelper.applySchematron(sch, source); 086 if (results == null) { 087 return; 088 } 089 090 IErrorList errors = SchematronHelper.convertToErrorList(results, theCtx.getFhirContext().getResourceDefinition(theCtx.getResource()).getBaseDefinition().getName()); 091 092 if (errors.getAllErrors().containsOnlySuccess()) { 093 return; 094 } 095 096 for (IError next : errors) { 097 ResultSeverityEnum severity; 098 if (next.isFailure()) { 099 severity = ResultSeverityEnum.ERROR; 100 } else if (next.isError()) { 101 severity = ResultSeverityEnum.FATAL; 102 } else if (next.isNoError()) { 103 severity = ResultSeverityEnum.WARNING; 104 } else { 105 continue; 106 } 107 108 String details = next.getAsString(Locale.getDefault()); 109 110 SingleValidationMessage message = new SingleValidationMessage(); 111 message.setMessage(details); 112 message.setLocationLine(next.getErrorLocation().getLineNumber()); 113 message.setLocationCol(next.getErrorLocation().getColumnNumber()); 114 message.setLocationString(next.getErrorLocation().getAsString()); 115 message.setSeverity(severity); 116 theCtx.addValidationMessage(message); 117 } 118 119 } 120 121 private ISchematronResource getSchematron(IValidationContext<IBaseResource> theCtx) { 122 Class<? extends IBaseResource> resource = theCtx.getResource().getClass(); 123 Class<? extends IBaseResource> baseResourceClass = theCtx.getFhirContext().getResourceDefinition(resource).getBaseDefinition().getImplementingClass(); 124 125 return getSchematronAndCache(theCtx, baseResourceClass); 126 } 127 128 private ISchematronResource getSchematronAndCache(IValidationContext<IBaseResource> theCtx, Class<? extends IBaseResource> theClass) { 129 synchronized (myClassToSchematron) { 130 ISchematronResource retVal = myClassToSchematron.get(theClass); 131 if (retVal != null) { 132 return retVal; 133 } 134 135 String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theCtx.getFhirContext().getResourceDefinition(theCtx.getResource()).getBaseDefinition().getName().toLowerCase() 136 + ".sch"; 137 try (InputStream baseIs = FhirValidator.class.getResourceAsStream(pathToBase)) { 138 if (baseIs == null) { 139 throw new InternalErrorException("Failed to load schematron for resource '" + theCtx.getFhirContext().getResourceDefinition(theCtx.getResource()).getBaseDefinition().getName() + "'. " 140 + SchemaBaseValidator.RESOURCES_JAR_NOTE); 141 } 142 } catch (IOException e) { 143 ourLog.error("Failed to close stream", e); 144 } 145 146 retVal = SchematronResourceSCH.fromClassPath(pathToBase); 147 myClassToSchematron.put(theClass, retVal); 148 return retVal; 149 } 150 } 151}