001package ca.uhn.fhir.validation.schematron; 002 003/* 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2016 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 java.io.InputStream; 024import java.io.StringReader; 025import java.util.HashMap; 026import java.util.Locale; 027import java.util.Map; 028 029import javax.xml.transform.stream.StreamSource; 030 031import org.apache.commons.io.IOUtils; 032import org.hl7.fhir.instance.model.api.IBaseResource; 033import org.oclc.purl.dsdl.svrl.SchematronOutputType; 034 035import com.phloc.commons.error.IResourceError; 036import com.phloc.commons.error.IResourceErrorGroup; 037import com.phloc.schematron.ISchematronResource; 038import com.phloc.schematron.SchematronHelper; 039import com.phloc.schematron.xslt.SchematronResourceSCH; 040 041import ca.uhn.fhir.context.FhirContext; 042import ca.uhn.fhir.model.api.Bundle; 043import ca.uhn.fhir.model.api.BundleEntry; 044import ca.uhn.fhir.rest.server.EncodingEnum; 045import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 046import ca.uhn.fhir.validation.FhirValidator; 047import ca.uhn.fhir.validation.IValidationContext; 048import ca.uhn.fhir.validation.IValidatorModule; 049import ca.uhn.fhir.validation.ResultSeverityEnum; 050import ca.uhn.fhir.validation.SchemaBaseValidator; 051import ca.uhn.fhir.validation.SingleValidationMessage; 052import ca.uhn.fhir.validation.ValidationContext; 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 Map<Class<? extends IBaseResource>, ISchematronResource> myClassToSchematron = new HashMap<Class<? extends IBaseResource>, ISchematronResource>(); 061 private FhirContext myCtx; 062 063 public SchematronBaseValidator(FhirContext theContext) { 064 myCtx = theContext; 065 } 066 067 @Override 068 public void validateResource(IValidationContext<IBaseResource> theCtx) { 069 070 ISchematronResource sch = getSchematron(theCtx); 071 String resourceAsString; 072 if (theCtx.getResourceAsStringEncoding() == EncodingEnum.XML) { 073 resourceAsString = theCtx.getResourceAsString(); 074 } else { 075 resourceAsString = theCtx.getFhirContext().newXmlParser().encodeResourceToString(theCtx.getResource()); 076 } 077 StreamSource source = new StreamSource(new StringReader(resourceAsString)); 078 079 SchematronOutputType results = SchematronHelper.applySchematron(sch, source); 080 if (results == null) { 081 return; 082 } 083 084 IResourceErrorGroup errors = SchematronHelper.convertToResourceErrorGroup(results, theCtx.getFhirContext().getResourceDefinition(theCtx.getResource()).getBaseDefinition().getName()); 085 086 if (errors.getAllErrors().containsOnlySuccess()) { 087 return; 088 } 089 090 for (IResourceError next : errors.getAllErrors().getAllResourceErrors()) { 091 ResultSeverityEnum severity; 092 switch (next.getErrorLevel()) { 093 case ERROR: 094 severity = ResultSeverityEnum.ERROR; 095 break; 096 case FATAL_ERROR: 097 severity = ResultSeverityEnum.FATAL; 098 break; 099 case WARN: 100 severity = ResultSeverityEnum.WARNING; 101 break; 102 case INFO: 103 case SUCCESS: 104 default: 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.getLocation().getLineNumber()); 113 message.setLocationCol(next.getLocation().getColumnNumber()); 114 message.setLocationString(next.getLocation().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() + ".sch"; 136 InputStream baseIs = FhirValidator.class.getResourceAsStream(pathToBase); 137 try { 138 if (baseIs == null) { 139 throw new InternalErrorException("Failed to load schematron for resource '" + theCtx.getFhirContext().getResourceDefinition(theCtx.getResource()).getBaseDefinition().getName() + "'. " + SchemaBaseValidator.RESOURCES_JAR_NOTE); 140 } 141 } finally { 142 IOUtils.closeQuietly(baseIs); 143 } 144 145 retVal = SchematronResourceSCH.fromClassPath(pathToBase); 146 myClassToSchematron.put(theClass, retVal); 147 return retVal; 148 } 149 } 150 151 @Override 152 public void validateBundle(IValidationContext<Bundle> theContext) { 153 for (BundleEntry next : theContext.getResource().getEntries()) { 154 if (next.getResource() != null) { 155 IValidationContext<IBaseResource> ctx = ValidationContext.newChild(theContext, next.getResource()); 156 validateResource(ctx); 157 } 158 } 159 } 160 161}