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}