001package org.hl7.fhir.dstu2016may.hapi.validation; 002 003import java.io.StringReader; 004import java.util.*; 005 006import javax.xml.parsers.DocumentBuilder; 007import javax.xml.parsers.DocumentBuilderFactory; 008 009import org.apache.commons.lang3.Validate; 010import org.hl7.fhir.dstu2016may.model.OperationOutcome.IssueSeverity; 011import org.hl7.fhir.dstu2016may.model.StructureDefinition; 012import org.hl7.fhir.dstu2016may.validation.*; 013import org.hl7.fhir.dstu2016may.validation.IResourceValidator.BestPracticeWarningLevel; 014import org.hl7.fhir.utilities.validation.ValidationMessage; 015import org.w3c.dom.*; 016import org.xml.sax.InputSource; 017 018import com.google.gson.*; 019 020import ca.uhn.fhir.context.ConfigurationException; 021import ca.uhn.fhir.context.FhirContext; 022import ca.uhn.fhir.rest.api.EncodingEnum; 023import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 024import ca.uhn.fhir.validation.IValidationContext; 025import ca.uhn.fhir.validation.IValidatorModule; 026 027public class FhirInstanceValidator extends BaseValidatorBridge implements IValidatorModule { 028 029 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirInstanceValidator.class); 030 private BestPracticeWarningLevel myBestPracticeWarningLevel; 031 private DocumentBuilderFactory myDocBuilderFactory; 032 private StructureDefinition myStructureDefintion; 033 private IValidationSupport myValidationSupport; 034 035 /** 036 * Constructor 037 * 038 * Uses {@link DefaultProfileValidationSupport} for {@link IValidationSupport validation support} 039 */ 040 public FhirInstanceValidator() { 041 this(new DefaultProfileValidationSupport()); 042 } 043 044 /** 045 * Constructor which uses the given validation support 046 * 047 * @param theValidationSupport 048 * The validation support 049 */ 050 public FhirInstanceValidator(IValidationSupport theValidationSupport) { 051 myDocBuilderFactory = DocumentBuilderFactory.newInstance(); 052 myDocBuilderFactory.setNamespaceAware(true); 053 myValidationSupport = theValidationSupport; 054 } 055 056 private String determineResourceName(Document theDocument) { 057 Element root = null; 058 059 NodeList list = theDocument.getChildNodes(); 060 for (int i = 0; i < list.getLength(); i++) { 061 if (list.item(i) instanceof Element) { 062 root = (Element) list.item(i); 063 break; 064 } 065 } 066 root = theDocument.getDocumentElement(); 067 return root.getLocalName(); 068 } 069 070 /** 071 * Returns the "best practice" warning level (default is {@link BestPracticeWarningLevel#Hint}). 072 * <p> 073 * The FHIR Instance Validator has a number of checks for best practices in terms of FHIR usage. If this setting is 074 * set to {@link BestPracticeWarningLevel#Error}, any resource data which does not meet these best practices will be 075 * reported at the ERROR level. If this setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice 076 * guielines will be ignored. 077 * </p> 078 * 079 * @see {@link #setBestPracticeWarningLevel(BestPracticeWarningLevel)} 080 */ 081 public BestPracticeWarningLevel getBestPracticeWarningLevel() { 082 return myBestPracticeWarningLevel; 083 } 084 085 /** 086 * Returns the {@link IValidationSupport validation support} in use by this validator. Default is an instance of 087 * {@link DefaultProfileValidationSupport} if the no-arguments constructor for this object was used. 088 */ 089 public IValidationSupport getValidationSupport() { 090 return myValidationSupport; 091 } 092 093 /** 094 * Sets the "best practice warning level". When validating, any deviations from best practices will be reported at 095 * this level. 096 * <p> 097 * The FHIR Instance Validator has a number of checks for best practices in terms of FHIR usage. If this setting is 098 * set to {@link BestPracticeWarningLevel#Error}, any resource data which does not meet these best practices will be 099 * reported at the ERROR level. If this setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice 100 * guielines will be ignored. 101 * </p> 102 * 103 * @param theBestPracticeWarningLevel 104 * The level, must not be <code>null</code> 105 */ 106 public void setBestPracticeWarningLevel(BestPracticeWarningLevel theBestPracticeWarningLevel) { 107 Validate.notNull(theBestPracticeWarningLevel); 108 myBestPracticeWarningLevel = theBestPracticeWarningLevel; 109 } 110 111 public void setStructureDefintion(StructureDefinition theStructureDefintion) { 112 myStructureDefintion = theStructureDefintion; 113 } 114 115 /** 116 * Sets the {@link IValidationSupport validation support} in use by this validator. Default is an instance of 117 * {@link DefaultProfileValidationSupport} if the no-arguments constructor for this object was used. 118 */ 119 public void setValidationSupport(IValidationSupport theValidationSupport) { 120 myValidationSupport = theValidationSupport; 121 } 122 123 protected List<ValidationMessage> validate(final FhirContext theCtx, String theInput, EncodingEnum theEncoding) { 124 HapiWorkerContext workerContext = new HapiWorkerContext(theCtx, myValidationSupport); 125 126 InstanceValidator v; 127 try { 128 v = new InstanceValidator(workerContext); 129 } catch (Exception e) { 130 throw new ConfigurationException(e); 131 } 132 133 v.setBestPracticeWarningLevel(myBestPracticeWarningLevel); 134 v.setAnyExtensionsAllowed(true); 135 v.setResourceIdRule(IResourceValidator.IdStatus.OPTIONAL); 136 137 List<ValidationMessage> messages = new ArrayList<ValidationMessage>(); 138 139 if (theEncoding == EncodingEnum.XML) { 140 Document document; 141 try { 142 DocumentBuilder builder = myDocBuilderFactory.newDocumentBuilder(); 143 InputSource src = new InputSource(new StringReader(theInput)); 144 document = builder.parse(src); 145 } catch (Exception e2) { 146 ourLog.error("Failure to parse XML input", e2); 147 ValidationMessage m = new ValidationMessage(); 148 m.setLevel(ValidationMessage.IssueSeverity.FATAL); 149 m.setMessage("Failed to parse input, it does not appear to be valid XML:" + e2.getMessage()); 150 return Collections.singletonList(m); 151 } 152 153 String resourceName = determineResourceName(document); 154 StructureDefinition profile = findStructureDefinitionForResourceName(theCtx, resourceName); 155 if (profile != null) { 156 try { 157 v.validate(messages, document, profile); 158 } catch (Exception e) { 159 throw new InternalErrorException("Unexpected failure while validating resource", e); 160 } 161 } 162 } else if (theEncoding == EncodingEnum.JSON) { 163 Gson gson = new GsonBuilder().create(); 164 JsonObject json = gson.fromJson(theInput, JsonObject.class); 165 166 String resourceName = json.get("resourceType").getAsString(); 167 StructureDefinition profile = findStructureDefinitionForResourceName(theCtx, resourceName); 168 if (profile != null) { 169 try { 170 v.validate(messages, json, profile); 171 } catch (Exception e) { 172 throw new InternalErrorException("Unexpected failure while validating resource", e); 173 } 174 } 175 } else { 176 throw new IllegalArgumentException("Unknown encoding: " + theEncoding); 177 } 178 179 for (int i = 0; i < messages.size(); i++) { 180 ValidationMessage next = messages.get(i); 181 if ("Binding has no source, so can't be checked".equals(next.getMessage())) { 182 messages.remove(i); 183 i--; 184 } 185 } 186 return messages; 187 } 188 189 private StructureDefinition findStructureDefinitionForResourceName(final FhirContext theCtx, String resourceName) { 190 String sdName = "http://hl7.org/fhir/StructureDefinition/" + resourceName; 191 StructureDefinition profile = myStructureDefintion != null ? myStructureDefintion : myValidationSupport.fetchStructureDefinition(theCtx, sdName); 192 return profile; 193 } 194 195 @Override 196 protected List<ValidationMessage> validate(IValidationContext<?> theCtx) { 197 return validate(theCtx.getFhirContext(), theCtx.getResourceAsString(), theCtx.getResourceAsStringEncoding()); 198 } 199 200 201}