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}