package org.hl7.fhir.instance.hapi.validation; import static org.apache.commons.lang3.StringUtils.isBlank; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity; import org.hl7.fhir.instance.model.StructureDefinition; import org.hl7.fhir.instance.validation.IResourceValidator.BestPracticeWarningLevel; import org.hl7.fhir.instance.validation.ValidationMessage; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.validation.IValidationContext; import ca.uhn.fhir.validation.IValidatorModule; public class FhirInstanceValidator extends BaseValidatorBridge implements IValidatorModule { private static FhirContext ourHl7OrgCtx; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirInstanceValidator.class); private BestPracticeWarningLevel myBestPracticeWarningLevel; private DocumentBuilderFactory myDocBuilderFactory; private StructureDefinition myStructureDefintion; private IValidationSupport myValidationSupport; /** * Constructor * * Uses {@link DefaultProfileValidationSupport} for {@link IValidationSupport * validation support} */ public FhirInstanceValidator() { this(new DefaultProfileValidationSupport()); } /** * Constructor which uses the given validation support * * @param theValidationSupport * The validation support */ public FhirInstanceValidator(IValidationSupport theValidationSupport) { myDocBuilderFactory = DocumentBuilderFactory.newInstance(); myDocBuilderFactory.setNamespaceAware(true); myValidationSupport = theValidationSupport; } private String determineResourceName(Document theDocument) { Element root = null; NodeList list = theDocument.getChildNodes(); for (int i = 0; i < list.getLength(); i++) { if (list.item(i) instanceof Element) { root = (Element) list.item(i); break; } } root = theDocument.getDocumentElement(); return root.getLocalName(); } /** * Returns the "best practice" warning level (default is * {@link BestPracticeWarningLevel#Hint}). * <p> * The FHIR Instance Validator has a number of checks for best practices in * terms of FHIR usage. If this setting is set to * {@link BestPracticeWarningLevel#Error}, any resource data which does not * meet these best practices will be reported at the ERROR level. If this * setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice * guielines will be ignored. * </p> * * @see {@link #setBestPracticeWarningLevel(BestPracticeWarningLevel)} */ public BestPracticeWarningLevel getBestPracticeWarningLevel() { return myBestPracticeWarningLevel; } /** * Returns the {@link IValidationSupport validation support} in use by this * validator. Default is an instance of * {@link DefaultProfileValidationSupport} if the no-arguments constructor for * this object was used. */ public IValidationSupport getValidationSupport() { return myValidationSupport; } /** * Sets the "best practice warning level". When validating, any deviations * from best practices will be reported at this level. * <p> * The FHIR Instance Validator has a number of checks for best practices in * terms of FHIR usage. If this setting is set to * {@link BestPracticeWarningLevel#Error}, any resource data which does not * meet these best practices will be reported at the ERROR level. If this * setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice * guielines will be ignored. * </p> * * @param theBestPracticeWarningLevel * The level, must not be <code>null</code> */ public void setBestPracticeWarningLevel(BestPracticeWarningLevel theBestPracticeWarningLevel) { Validate.notNull(theBestPracticeWarningLevel); myBestPracticeWarningLevel = theBestPracticeWarningLevel; } public void setStructureDefintion(StructureDefinition theStructureDefintion) { myStructureDefintion = theStructureDefintion; } /** * Sets the {@link IValidationSupport validation support} in use by this * validator. Default is an instance of * {@link DefaultProfileValidationSupport} if the no-arguments constructor for * this object was used. */ public void setValidationSupport(IValidationSupport theValidationSupport) { myValidationSupport = theValidationSupport; } protected List<ValidationMessage> validate(final FhirContext theCtx, String theInput, EncodingEnum theEncoding) { HapiWorkerContext workerContext = new HapiWorkerContext(theCtx, myValidationSupport); org.hl7.fhir.instance.validation.InstanceValidator v; try { v = new org.hl7.fhir.instance.validation.InstanceValidator(workerContext); } catch (Exception e) { throw new ConfigurationException(e); } v.setShouldCheckForIdPresence(false); v.setBestPracticeWarningLevel(myBestPracticeWarningLevel); v.setAnyExtensionsAllowed(true); v.setRequireResourceId(false); v.setSuppressLoincSnomedMessages(true); List<ValidationMessage> messages = new ArrayList<ValidationMessage>(); if (theEncoding == EncodingEnum.XML) { Document document; try { DocumentBuilder builder = myDocBuilderFactory.newDocumentBuilder(); InputSource src = new InputSource(new StringReader(theInput)); document = builder.parse(src); } catch (Exception e2) { ourLog.error("Failure to parse XML input", e2); ValidationMessage m = new ValidationMessage(); m.setLevel(IssueSeverity.FATAL); m.setMessage("Failed to parse input, it does not appear to be valid XML:" + e2.getMessage()); return Collections.singletonList(m); } String resourceName = determineResourceName(document); StructureDefinition profile = myStructureDefintion != null ? myStructureDefintion : loadProfileOrReturnNull(messages, theCtx, resourceName); if (profile != null) { try { v.validate(messages, document, profile); } catch (Exception e) { throw new InternalErrorException("Unexpected failure while validating resource", e); } } } else if (theEncoding == EncodingEnum.JSON) { Gson gson = new GsonBuilder().create(); JsonObject json = gson.fromJson(theInput, JsonObject.class); String resourceName = json.get("resourceType").getAsString(); StructureDefinition profile = myStructureDefintion != null ? myStructureDefintion : loadProfileOrReturnNull(messages, theCtx, resourceName); if (profile != null) { try { v.validate(messages, json, profile); } catch (Exception e) { throw new InternalErrorException("Unexpected failure while validating resource", e); } } } else { throw new IllegalArgumentException("Unknown encoding: " + theEncoding); } for (int i = 0; i < messages.size(); i++) { ValidationMessage next = messages.get(i); if ("Binding has no source, so can't be checked".equals(next.getMessage())) { messages.remove(i); i--; } } return messages; } @Override protected List<ValidationMessage> validate(IValidationContext<?> theCtx) { return validate(theCtx.getFhirContext(), theCtx.getResourceAsString(), theCtx.getResourceAsStringEncoding()); } static FhirContext getHl7OrgDstu2Ctx(FhirContext theCtx) { if (theCtx.getVersion().getVersion() == FhirVersionEnum.DSTU2_HL7ORG) { return theCtx; } FhirContext retVal = ourHl7OrgCtx; if (retVal == null) { retVal = FhirContext.forDstu2Hl7Org(); ourHl7OrgCtx = retVal; } return retVal; } static StructureDefinition loadProfileOrReturnNull(List<ValidationMessage> theMessages, FhirContext theCtx, String theResourceName) { if (isBlank(theResourceName)) { if (theMessages != null) { theMessages.add(new ValidationMessage().setLevel(IssueSeverity.FATAL) .setMessage("Could not determine resource type from request. Content appears invalid.")); } return null; } String profileClasspath = theCtx.getVersion().getPathToSchemaDefinitions().replace("/schema", "/profile"); String profileCpName = profileClasspath + '/' + theResourceName.toLowerCase() + ".profile.xml"; String profileText; try { InputStream inputStream = FhirInstanceValidator.class.getResourceAsStream(profileCpName); if (inputStream == null) { if (theMessages != null) { theMessages.add(new ValidationMessage().setLevel(IssueSeverity.FATAL) .setMessage("No profile found for resource type " + theResourceName)); return null; } else { return null; } } profileText = IOUtils.toString(inputStream, "UTF-8"); } catch (IOException e1) { if (theMessages != null) { theMessages.add(new ValidationMessage().setLevel(IssueSeverity.FATAL) .setMessage("No profile found for resource type " + theResourceName)); } return null; } StructureDefinition profile = getHl7OrgDstu2Ctx(theCtx).newXmlParser().parseResource(StructureDefinition.class, profileText); return profile; } }