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