package org.hl7.fhir.r4.validation;

import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.exceptions.*;
import org.hl7.fhir.r4.conformance.ProfileUtilities;
import org.hl7.fhir.r4.context.IWorkerContext;
import org.hl7.fhir.r4.context.IWorkerContext.ValidationResult;
import org.hl7.fhir.r4.elementmodel.*;
import org.hl7.fhir.r4.elementmodel.Element;
import org.hl7.fhir.r4.elementmodel.Element.SpecialElement;
import org.hl7.fhir.r4.elementmodel.Manager.FhirFormat;
import org.hl7.fhir.r4.elementmodel.ParserBase.ValidationPolicy;
import org.hl7.fhir.r4.formats.FormatUtilities;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent;
import org.hl7.fhir.r4.model.ElementDefinition.*;
import org.hl7.fhir.r4.model.Enumeration;
import org.hl7.fhir.r4.model.Enumerations.BindingStrength;
import org.hl7.fhir.r4.model.Questionnaire.*;
import org.hl7.fhir.r4.model.StructureDefinition.*;
import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent;
import org.hl7.fhir.r4.utils.*;
import org.hl7.fhir.r4.utils.FHIRLexer.FHIRLexerException;
import org.hl7.fhir.r4.utils.FHIRPathEngine.IEvaluationContext;
import org.hl7.fhir.r4.utils.ValidationProfileSet.ProfileRegistration;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.validation.ValidationMessage;
import org.hl7.fhir.utilities.validation.ValidationMessage.*;
import org.hl7.fhir.utilities.xhtml.NodeType;
import org.hl7.fhir.utilities.xhtml.XhtmlNode;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

import com.google.gson.Gson;
import com.google.gson.JsonObject;

import ca.uhn.fhir.util.ObjectUtil;


/**
 * Thinking of using this in a java program? Don't! 
 * You should use on of the wrappers instead. Either in HAPI, or use ValidationEngine
 * 
 * @author Grahame Grieve
 *
 */
/* 
 * todo:
 * check urn's don't start oid: or uuid: 
 */
public class InstanceValidator extends BaseValidator implements IResourceValidator {


  private class ValidatorHostContext {
    private Object appContext;
    private Element container; // bundle, or parameters
    private Element resource;
    public ValidatorHostContext(Object appContext) {
      this.appContext = appContext;
    }
    public ValidatorHostContext(Object appContext, Element element) {
      this.appContext = appContext;
      this.resource = element;
    }
    
    public ValidatorHostContext forContained(Element element) {
      ValidatorHostContext res = new ValidatorHostContext(appContext);
      res.resource = element;
      return res;
    }
  }
  
  private class ValidatorHostServices implements IEvaluationContext {

    @Override
    public Base resolveConstant(Object appContext, String name) throws PathEngineException {
      ValidatorHostContext c = (ValidatorHostContext) appContext;
      if (externalHostServices != null)
        return externalHostServices.resolveConstant(c.appContext, name);
      else
        return null;
    }

    @Override
    public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException {
      ValidatorHostContext c = (ValidatorHostContext) appContext;
      if (externalHostServices != null)
        return externalHostServices.resolveConstantType(c.appContext, name);
      else
        return null;
    }

    @Override
    public boolean log(String argument, List<Base> focus) {
      if (externalHostServices != null)
        return externalHostServices.log(argument, focus);
      else 
        return false;
    }

    @Override
    public FunctionDetails resolveFunction(String functionName) {
      throw new Error("Not done yet");
    }

    @Override
    public TypeDetails checkFunction(Object appContext, String functionName, List<TypeDetails> parameters) throws PathEngineException {
      throw new Error("Not done yet");
    }

    @Override
    public List<Base> executeFunction(Object appContext, String functionName, List<List<Base>> parameters) {
      throw new Error("Not done yet");
    }

    @Override
    public Base resolveReference(Object appContext, String url) {
      ValidatorHostContext c = (ValidatorHostContext) appContext;
      if (c.container != null || externalHostServices == null)
        throw new Error("Not done yet - resolve "+url+" locally");
      else 
        return externalHostServices.resolveReference(c.appContext, url);
          
    }

  }

  private IWorkerContext context;
  private FHIRPathEngine fpe; 

  // configuration items
  private CheckDisplayOption checkDisplay;
  private boolean anyExtensionsAllowed;
  private boolean errorForUnknownProfiles;
  private boolean noInvariantChecks;
  private boolean noTerminologyChecks;
  private BestPracticeWarningLevel bpWarnings;

  private List<String> extensionDomains = new ArrayList<String>();

  private IdStatus resourceIdRule;
  private boolean allowXsiLocation;

  // used during the build process to keep the overall volume of messages down
  private boolean suppressLoincSnomedMessages;

  private Bundle logical;

  // time tracking
  private long overall = 0;
  private long txTime = 0;
  private long sdTime = 0;
  private long loadTime = 0;
  private long fpeTime = 0;

  private boolean noBindingMsgSuppressed;
  private HashMap<Element, ResourceProfiles> resourceProfilesMap;
  private IValidatorResourceFetcher fetcher;
  long time = 0;
  private ValidationProfileSet providedProfiles;
  private IEvaluationContext externalHostServices;

  /*
   * Keeps track of whether a particular profile has been checked or not yet
   */
  private class ProfileUsage {
    private StructureDefinition profile;
    private boolean checked;

    public ProfileUsage(StructureDefinition profile) {
      this.profile = profile;
      this.checked = false;
    }

    public boolean isChecked() {
      return checked;
    }

    public void setChecked() {
      this.checked = true;
    }

    public StructureDefinition getProfile() {
      return profile;
    }
  }

  /*
   * Keeps track of all profiles associated with a resource element and whether the resource has been checked against those profiles yet
   */
  public class ResourceProfiles {
    private Element resource;
    private Element owner;
    private NodeStack stack;
    private HashMap<StructureDefinition, ProfileUsage> profiles;
    private boolean processed;

    public ResourceProfiles(Element resource, NodeStack stack) {
      this.resource = resource;
      if (this.resource.getName().equals("contained"))
        this.owner = stack.parent.element;
      else
        this.owner = resource;
      this.stack = stack;
      this.profiles = new HashMap<StructureDefinition, ProfileUsage>();
      this.processed = false;
    }

    public boolean isProcessed() {
      return processed;
    }

    public void setProcessed() {
      processed = true;
    }

    public NodeStack getStack() {
      return stack;
    }

    public Element getOwner() {
      return owner;
    }

    public boolean hasProfiles() {
      return !profiles.isEmpty();
    }

    public void addProfiles(List<ValidationMessage> errors, ValidationProfileSet profiles, String path, Element element, boolean external) throws FHIRException {
      for (ProfileRegistration profile : profiles.getCanonical()) {
        StructureDefinition sd = profiles.fetch(profile.getProfile());
        if (sd == null)
          sd = context.fetchResource(StructureDefinition.class, profile.getProfile());
        if (sd == null) {
          errors.add(new ValidationMessage(Source.InstanceValidator, IssueType.UNKNOWN, path, "Unable to locate profile "+profile.getProfile(), external ? IssueSeverity.ERROR : IssueSeverity.WARNING));
        } else if (!sd.getType().equals(element.fhirType())) {
          boolean ok = false;
          if (element.fhirType().equals("Bundle")) { // special case: if the profile type isn't 'Bundle', then the profile applies to the first resource
            List<Element> entries = element.getChildren("entry");
            if (entries.size() > 0) {
              Element res = entries.get(0).getNamedChild("resource");
              if (res != null) {
                ok = true;
          //       addProfile(errors, profile.getProfile(), profile.isError(), path, res);  ggtodo: we need to go add this to a different profile          
              }
            }
          }
            
          if (!ok)
            errors.add(new ValidationMessage(Source.InstanceValidator, IssueType.UNKNOWN, path, "Profile mismatch on type for "+profile.getProfile()+": the profile constrains "+sd.getType()+" but the element is "+element.fhirType(), IssueSeverity.ERROR));
        } else 
          addProfile(errors, profile.getProfile(), profile.isError(), path, element);          
      }
    }
    
    public boolean addProfile(List<ValidationMessage> errors, String profile, boolean error, String path, Element element) {
      String effectiveProfile = profile;
      String version = null;
      if (profile.contains("|")) {
        effectiveProfile = profile.substring(0, profile.indexOf('|'));
        version = profile.substring(profile.indexOf('|')+1);
      }
      StructureDefinition sd = null;
      if (providedProfiles != null)
        sd = providedProfiles.fetch(effectiveProfile);
      if (sd == null)
        sd = context.fetchResource(StructureDefinition.class, effectiveProfile);
      
      if (warningOrError(error, errors, IssueType.INVALID, element.line(), element.col(), path, sd != null, "StructureDefinition reference \"{0}\" could not be resolved", profile)) {
        if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, version==null || (sd.getVersion()!=null && sd.getVersion().equals(version)), 
             "Referenced version " + version + " does not match found version " + sd.getVersion() + " for profile " + sd.getUrl(), profile)) {
          if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, sd.hasSnapshot(),
              "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided")) {
            if (!profiles.containsKey(sd)) {
              profiles.put(sd,  new ProfileUsage(sd));
              addAncestorProfiles(sd);
              return true;
            }
          }
        }
      }
      return false;
    }

    public void addAncestorProfiles(StructureDefinition sd) {
      if (sd.hasDerivation() && sd.getDerivation().equals(StructureDefinition.TypeDerivationRule.CONSTRAINT)) {
        StructureDefinition parentSd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
        if (parentSd != null && !profiles.containsKey(parentSd)) {
          ProfileUsage pu = new ProfileUsage(parentSd);
          pu.setChecked(); // We're going to check the child, so no need to check the parent
          profiles.put(parentSd, pu);
        }
      }
    }

    public List<ProfileUsage> uncheckedProfiles() {
      List<ProfileUsage> uncheckedProfiles = new ArrayList<ProfileUsage>();
      for (ProfileUsage profileUsage : profiles.values()) {
        if (!profileUsage.isChecked())
          uncheckedProfiles.add(profileUsage);
      }
      return uncheckedProfiles;
    }

    public boolean hasUncheckedProfiles() {
      return !uncheckedProfiles().isEmpty();
    }

    public void checkProfile(StructureDefinition profile) {
      ProfileUsage profileUsage = profiles.get(profile);
      if (profileUsage==null)
        throw new Error("Can't check profile that hasn't been added: " + profile.getUrl());
      else
        profileUsage.setChecked();
    }
  }

  public InstanceValidator(IWorkerContext theContext, IEvaluationContext hostServices) {
    super();
    this.context = theContext;
    this.externalHostServices = hostServices;
    fpe = new FHIRPathEngine(context);
    fpe.setHostServices(new ValidatorHostServices());
    source = Source.InstanceValidator;
  }

  @Override
  public boolean isNoInvariantChecks() {
    return noInvariantChecks;
  }

  @Override
  public IResourceValidator setNoInvariantChecks(boolean value) {
    this.noInvariantChecks = value;
    return this;
  }

  public IValidatorResourceFetcher getFetcher() {
    return this.fetcher;
  }

  public IResourceValidator setFetcher(IValidatorResourceFetcher value) {
    this.fetcher = value;
    return this;
  }

  private boolean allowUnknownExtension(String url) {
    if (url.contains("example.org") || url.contains("acme.com") || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression"))
      // Added structuredefinition-expression explicitly because it wasn't defined in the version of the spec it needs to be used with
      return true;
    for (String s : extensionDomains)
      if (url.startsWith(s))
        return true;
    return anyExtensionsAllowed;
  }

  private boolean isKnownExtension(String url) {
    // Added structuredefinition-expression explicitly because it wasn't defined in the version of the spec it needs to be used with
    if (url.contains("example.org") || url.contains("acme.com") || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression"))
      return true;
    for (String s : extensionDomains)
      if (url.startsWith(s))
        return true;
    return false;
  }

  private void bpCheck(List<ValidationMessage> errors, IssueType invalid, int line, int col, String literalPath, boolean test, String message) {
    if (bpWarnings != null) {
      switch (bpWarnings) {
      case Error:
        rule(errors, invalid, line, col, literalPath, test, message);
        break;
      case Warning:
        warning(errors, invalid, line, col, literalPath, test, message);
        break;
      case Hint:
        hint(errors, invalid, line, col, literalPath, test, message);
        break;
      default: // do nothing
      }
    }
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, InputStream stream, FhirFormat format) throws FHIRException, IOException {
    return validate(appContext, errors, stream, format, new ValidationProfileSet());
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, InputStream stream, FhirFormat format, String profile) throws FHIRException, IOException {
    return validate(appContext, errors, stream, format,  new ValidationProfileSet(profile, true));
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, InputStream stream, FhirFormat format, StructureDefinition profile) throws FHIRException, IOException {
    return validate(appContext, errors, stream, format, new ValidationProfileSet(profile));
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, InputStream stream, FhirFormat format, ValidationProfileSet profiles) throws FHIRException, IOException {
    ParserBase parser = Manager.makeParser(context, format);
    if (parser instanceof XmlParser) 
      ((XmlParser) parser).setAllowXsiLocation(allowXsiLocation);
    parser.setupValidation(ValidationPolicy.EVERYTHING, errors); 
    long t = System.nanoTime();
    Element e = parser.parse(stream);
    loadTime = System.nanoTime() - t;
    if (e != null)
      validate(appContext, errors, e, profiles);
    return e;
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Resource resource) throws FHIRException, IOException {
    return validate(appContext, errors, resource, new ValidationProfileSet());
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Resource resource, String profile) throws FHIRException, IOException {
    return validate(appContext, errors, resource, new ValidationProfileSet(profile, true));
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Resource resource, StructureDefinition profile) throws FHIRException, IOException {
    return validate(appContext, errors, resource, new ValidationProfileSet(profile));
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Resource resource, ValidationProfileSet profiles) throws FHIRException, IOException {
    long t = System.nanoTime();
    Element e = new ObjectConverter(context).convert(resource);
    loadTime = System.nanoTime() - t;
    validate(appContext, errors, e, profiles);
    return e;
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, org.w3c.dom.Element element) throws FHIRException, IOException {
    return validate(appContext, errors, element, new ValidationProfileSet());
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, org.w3c.dom.Element element, String profile) throws FHIRException, IOException {
    return validate(appContext, errors, element, new ValidationProfileSet(profile, true));
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, org.w3c.dom.Element element, StructureDefinition profile) throws FHIRException, IOException {
    return validate(appContext, errors, element, new ValidationProfileSet(profile));
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, org.w3c.dom.Element element, ValidationProfileSet profiles) throws FHIRException, IOException {
    XmlParser parser = new XmlParser(context);
    parser.setupValidation(ValidationPolicy.EVERYTHING, errors); 
    long t = System.nanoTime();
    Element e = parser.parse(element);
    loadTime = System.nanoTime() - t;
    if (e != null) 
      validate(appContext, errors, e, profiles);
    return e;
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Document document) throws FHIRException, IOException {
    return validate(appContext, errors, document, new ValidationProfileSet());
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Document document, String profile) throws FHIRException, IOException {
    return validate(appContext, errors, document, new ValidationProfileSet(profile, true));
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Document document, StructureDefinition profile) throws FHIRException, IOException {
    return validate(appContext, errors, document, new ValidationProfileSet(profile));
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, Document document, ValidationProfileSet profiles) throws FHIRException, IOException {
    XmlParser parser = new XmlParser(context);
    parser.setupValidation(ValidationPolicy.EVERYTHING, errors); 
    long t = System.nanoTime();
    Element e = parser.parse(document);
    loadTime = System.nanoTime() - t;
    if (e != null)
      validate(appContext, errors, e, profiles);
    return e;
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, JsonObject object) throws FHIRException, IOException {
    return validate(appContext, errors, object, new ValidationProfileSet());
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, JsonObject object, String profile) throws FHIRException, IOException {
    return validate(appContext, errors, object, new ValidationProfileSet(profile, true));
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, JsonObject object, StructureDefinition profile) throws FHIRException, IOException {
    return validate(appContext, errors, object, new ValidationProfileSet(profile));
  }

  @Override
  public org.hl7.fhir.r4.elementmodel.Element validate(Object appContext, List<ValidationMessage> errors, JsonObject object, ValidationProfileSet profiles) throws FHIRException, IOException {
    JsonParser parser = new JsonParser(context);
    parser.setupValidation(ValidationPolicy.EVERYTHING, errors); 
    long t = System.nanoTime();
    Element e = parser.parse(object);
    loadTime = System.nanoTime() - t;
    if (e != null)
      validate(appContext, errors, e, profiles);
    return e;
  }

  @Override
  public void validate(Object appContext, List<ValidationMessage> errors, Element element) throws FHIRException, IOException {
    ValidationProfileSet profileSet = new ValidationProfileSet();
    validate(appContext, errors, element, profileSet);
  }

  private void validateRemainder(Object appContext, List<ValidationMessage> errors) throws IOException, FHIRException {
    boolean processedResource;
    do {
      processedResource = false;
      Set<Element> keys = new HashSet<Element>();
      keys.addAll(resourceProfilesMap.keySet());
      for (Element resource : keys) {
        ResourceProfiles rp = resourceProfilesMap.get(resource);
        if (rp.hasUncheckedProfiles()) {
          processedResource = true;
          start(new ValidatorHostContext(appContext), errors, rp.getOwner(), resource, null, rp.getStack());
        }
      }
    } while (processedResource);
  }
  
  @Override
  public void validate(Object appContext, List<ValidationMessage> errors, Element element, String profile) throws FHIRException, IOException {
    validate(appContext, errors, element, new ValidationProfileSet(profile, true));
  }

  @Override
  public void validate(Object appContext, List<ValidationMessage> errors, Element element, StructureDefinition profile) throws FHIRException, IOException {
    validate(appContext, errors, element, new ValidationProfileSet(profile));
  }

  @Override
  public void validate(Object appContext, List<ValidationMessage> errors, Element element, ValidationProfileSet profiles) throws FHIRException, IOException {
    // this is the main entry point; all the other entry points end up here coming here...
    providedProfiles = profiles;
    long t = System.nanoTime();
    boolean isRoot = false;
    if (resourceProfilesMap == null) {
      resourceProfilesMap = new HashMap<Element, ResourceProfiles>();
      isRoot = true;
    }
    validateResource(new ValidatorHostContext(appContext, element), errors, element, element, null, profiles, resourceIdRule, new NodeStack(element), true);
    if (isRoot) {
      validateRemainder(appContext, errors);
      resourceProfilesMap = null;
      checkElementUsage(errors, element, new NodeStack(element));
    }
    overall = System.nanoTime() - t;
  }

  private void checkElementUsage(List<ValidationMessage> errors, Element element, NodeStack stack) {
    String elementUsage = element.getUserString("elementSupported");
    hint(errors, IssueType.INFORMATIONAL, element.line(),element.col(), stack.getLiteralPath(), elementUsage==null || elementUsage.equals("Y"), "Instance includes element that is not marked as 'mustSupport' and was validated against profiles declaring mustSupport=true");
    if (element.hasChildren()) {
      String prevName = "";
      int elementCount = 0;
      for (Element ce : element.getChildren()) {
        if (ce.getName().equals(prevName))
          elementCount++;
        else {
          elementCount=1;
          prevName = ce.getName();
        }
        checkElementUsage(errors, ce, stack.push(ce, elementCount, null, null));
      }
    }
  }
  
  private boolean check(String v1, String v2) {
    return v1 == null ? Utilities.noString(v1) : v1.equals(v2);
  }

  private void checkAddress(List<ValidationMessage> errors, String path, Element focus, Address fixed) {
    checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use", focus);
    checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text", focus);
    checkFixedValue(errors, path + ".city", focus.getNamedChild("city"), fixed.getCityElement(), "city", focus);
    checkFixedValue(errors, path + ".state", focus.getNamedChild("state"), fixed.getStateElement(), "state", focus);
    checkFixedValue(errors, path + ".country", focus.getNamedChild("country"), fixed.getCountryElement(), "country", focus);
    checkFixedValue(errors, path + ".zip", focus.getNamedChild("zip"), fixed.getPostalCodeElement(), "postalCode", focus);

    List<Element> lines = new ArrayList<Element>();
    focus.getNamedChildren("line", lines);
    if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, lines.size() == fixed.getLine().size(),
        "Expected " + Integer.toString(fixed.getLine().size()) + " but found " + Integer.toString(lines.size()) + " line elements")) {
      for (int i = 0; i < lines.size(); i++)
        checkFixedValue(errors, path + ".coding", lines.get(i), fixed.getLine().get(i), "coding", focus);
    }
  }

  private void checkAttachment(List<ValidationMessage> errors, String path, Element focus, Attachment fixed) {
    checkFixedValue(errors, path + ".contentType", focus.getNamedChild("contentType"), fixed.getContentTypeElement(), "contentType", focus);
    checkFixedValue(errors, path + ".language", focus.getNamedChild("language"), fixed.getLanguageElement(), "language", focus);
    checkFixedValue(errors, path + ".data", focus.getNamedChild("data"), fixed.getDataElement(), "data", focus);
    checkFixedValue(errors, path + ".url", focus.getNamedChild("url"), fixed.getUrlElement(), "url", focus);
    checkFixedValue(errors, path + ".size", focus.getNamedChild("size"), fixed.getSizeElement(), "size", focus);
    checkFixedValue(errors, path + ".hash", focus.getNamedChild("hash"), fixed.getHashElement(), "hash", focus);
    checkFixedValue(errors, path + ".title", focus.getNamedChild("title"), fixed.getTitleElement(), "title", focus);
  }

  // public API

  private boolean checkCode(List<ValidationMessage> errors, Element element, String path, String code, String system, String display) throws TerminologyServiceException {
    long t = System.nanoTime();
    boolean ss = context.supportsSystem(system);
    txTime = txTime + (System.nanoTime() - t);
    if (ss) {
      t = System.nanoTime();
      ValidationResult s = context.validateCode(system, code, display);
      txTime = txTime + (System.nanoTime() - t);
      if (s == null) 
        return true;
      if (s.isOk()) {
        if (s.getMessage() != null)
          warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage());
        return true;
      }
      if (s.getSeverity() == IssueSeverity.INFORMATION)
        hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage());
      else if (s.getSeverity() == IssueSeverity.WARNING)
        warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage());
      else
        return rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, s == null, s.getMessage());
      return true;
    } else if (system.startsWith("http://hl7.org/fhir")) {
      if (system.equals("http://hl7.org/fhir/sid/icd-10"))
        return true; // else don't check ICD-10 (for now)
      else {
        CodeSystem cs = getCodeSystem(system);
        if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, cs != null, "Unknown Code System " + system)) {
          ConceptDefinitionComponent def = getCodeDefinition(cs, code);
          if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, def != null, "Unknown Code (" + system + "#" + code + ")"))
            return warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, display == null || display.equals(def.getDisplay()), "Display should be '" + def.getDisplay() + "'");
        }
        return false;
      }
    } else if (context.isNoTerminologyServer() &&  Utilities.existsInList(system, "http://loinc.org", "http://unitsofmeasure.org", "http://snomed.info/sct", "http://www.nlm.nih.gov/research/umls/rxnorm")) {
      return true; // no checks in this case
    } else if (startsWithButIsNot(system, "http://snomed.info/sct", "http://loinc.org", "http://unitsofmeasure.org", "http://www.nlm.nih.gov/research/umls/rxnorm")) {
      rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Invalid System URI: "+system);
      return false;
    } else {
      try {
        if (context.fetchResourceWithException(ValueSet.class, system) != null) {
          rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Invalid System URI: "+system+" - cannot use a value set URI as a system");
          // Lloyd: This error used to prohibit checking for downstream issues, but there are some cases where that checking needs to occur.  Please talk to me before changing the code back. 
        }
        return true;
      }
      catch (Exception e) {
        return true;
      }
    }
  }

  private boolean startsWithButIsNot(String system, String... uri) {
    for (String s : uri)
      if (!system.equals(s) && system.startsWith(s))
        return true;
    return false;
  }

  private void checkCodeableConcept(List<ValidationMessage> errors, String path, Element focus, CodeableConcept fixed) {
    checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text", focus);
    List<Element> codings = new ArrayList<Element>();
    focus.getNamedChildren("coding", codings);
    if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, codings.size() == fixed.getCoding().size(),
        "Expected " + Integer.toString(fixed.getCoding().size()) + " but found " + Integer.toString(codings.size()) + " coding elements")) {
      for (int i = 0; i < codings.size(); i++)
        checkFixedValue(errors, path + ".coding", codings.get(i), fixed.getCoding().get(i), "coding", focus);
    }
  }

  private void checkCodeableConcept(List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext)  {
    if (!noTerminologyChecks && theElementCntext != null && theElementCntext.hasBinding()) {
      ElementDefinitionBindingComponent binding = theElementCntext.getBinding();
      if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, "Binding for " + path + " missing (cc)")) {
        if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) {
          ValueSet valueset = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl());
          if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(binding.getValueSet()) + " not found")) {
            try {
              CodeableConcept cc = ObjectConverter.readAsCodeableConcept(element);
              if (!cc.hasCoding()) {
                if (binding.getStrength() == BindingStrength.REQUIRED)
                  rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "No code provided, and a code is required from the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl());
                else if (binding.getStrength() == BindingStrength.EXTENSIBLE) 
                  warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "No code provided, and a code should be provided from the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl());
              } else {
                long t = System.nanoTime();

                // Check whether the codes are appropriate for the type of binding we have
                boolean bindingsOk = true;
                if (binding.getStrength() != BindingStrength.EXAMPLE) {
                  boolean atLeastOneSystemIsSupported = false;
                  for (Coding nextCoding : cc.getCoding()) {
                    String nextSystem = nextCoding.getSystem();
                    if (isNotBlank(nextSystem) && context.supportsSystem(nextSystem)) {
                      atLeastOneSystemIsSupported = true;
                      break;
                    }
                  }

                  if (!atLeastOneSystemIsSupported && binding.getStrength() == BindingStrength.EXAMPLE) {
                    // ignore this since we can't validate but it doesn't matter..
                  } else {
                    ValidationResult vr = context.validateCode(cc, valueset);
                    if (!vr.isOk()) {
                      bindingsOk = false;
                      if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) {
                        if (binding.getStrength() == BindingStrength.REQUIRED)
                          warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " and a code from this value set is required (class = "+vr.getErrorClass().toString()+")");
                        else if (binding.getStrength() == BindingStrength.EXTENSIBLE) {
                          if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) 
                            checkMaxValueSet(errors, path, element, profile, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), cc);
                          else
                            warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " and a code should come from this value set unless it has no suitable code (class = "+vr.getErrorClass().toString()+")");
                        } else if (binding.getStrength() == BindingStrength.PREFERRED)
                          hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,  "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " and a code is recommended to come from this value set (class = "+vr.getErrorClass().toString()+")");
                      } else {
                        if (binding.getStrength() == BindingStrength.REQUIRED)
                          rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl()+", and a code from this value set is required) (codes = "+ccSummary(cc)+")");
                        else if (binding.getStrength() == BindingStrength.EXTENSIBLE) {
                          if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) 
                            checkMaxValueSet(errors, path, element, profile, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), cc);
                          else
                            warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code should come from this value set unless it has no suitable code) (codes = "+ccSummary(cc)+")");
                        } else if (binding.getStrength() == BindingStrength.PREFERRED)
                          hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,  "None of the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code is recommended to come from this value set) (codes = "+ccSummary(cc)+")");
                      }
                    } else if (vr.getMessage()!=null)
                      warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, vr.getMessage());
                  }
                  // Then, for any codes that are in code systems we are able
                  // to validate, we'll validate that the codes actually exist
                  if (bindingsOk) {
                    for (Coding nextCoding : cc.getCoding()) {
                      String nextCode = nextCoding.getCode();
                      String nextSystem = nextCoding.getSystem();
                      if (isNotBlank(nextCode) && isNotBlank(nextSystem) && context.supportsSystem(nextSystem)) {
                        ValidationResult vr = context.validateCode(nextSystem, nextCode, null);
                        if (!vr.isOk()) {
                          warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Code {0} is not a valid code in code system {1}", nextCode, nextSystem);
                        }
                      }
                    }
                  }
                  txTime = txTime + (System.nanoTime() - t);
                }
              }
            } catch (Exception e) {
              warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error "+e.getMessage()+" validating CodeableConcept");
            }
          }
        } else if (binding.hasValueSet()) {
          hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding by URI reference cannot be checked");
        } else if (!noBindingMsgSuppressed) {
          hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding for path " + path + " has no source, so can't be checked");
        }
      }
    }
  }

  private void checkMaxValueSet(List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, Reference maxVSUrl, CodeableConcept cc) {
    // TODO Auto-generated method stub
    ValueSet valueset = resolveBindingReference(profile, maxVSUrl, profile.getUrl());
    if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(maxVSUrl) + " not found")) {
      try {
        long t = System.nanoTime();
        ValidationResult vr = context.validateCode(cc, valueset);
        txTime = txTime + (System.nanoTime() - t);
        if (!vr.isOk()) {
          rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "None of the codes provided are in the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl()+", and a code from this value set is required) (codes = "+ccSummary(cc)+")");
        }
      } catch (Exception e) {
        warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error "+e.getMessage()+" validating CodeableConcept using maxValueSet");
      }
    }
  }

  private void checkMaxValueSet(List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, Reference maxVSUrl, Coding c) {
    // TODO Auto-generated method stub
    ValueSet valueset = resolveBindingReference(profile, maxVSUrl, profile.getUrl());
    if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(maxVSUrl) + " not found")) {
      try {
        long t = System.nanoTime();
        ValidationResult vr = context.validateCode(c, valueset);
        txTime = txTime + (System.nanoTime() - t);
        if (!vr.isOk()) {
          rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The code provided is not in the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl()+", and a code from this value set is required) (code = "+c.getSystem()+"#"+c.getCode()+")");
        }
      } catch (Exception e) {
        warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error "+e.getMessage()+" validating CodeableConcept using maxValueSet");
      }
    }
  }

  private void checkMaxValueSet(List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, Reference maxVSUrl, String value) {
    // TODO Auto-generated method stub
    ValueSet valueset = resolveBindingReference(profile, maxVSUrl, profile.getUrl());
    if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(maxVSUrl) + " not found")) {
      try {
        long t = System.nanoTime();
        ValidationResult vr = context.validateCode(null, value, null, valueset);
        txTime = txTime + (System.nanoTime() - t);
        if (!vr.isOk()) {
          if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) 
            warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The code provided could not be validated against the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl()+"), (error = "+vr.getMessage()+")");
          else
            rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The code provided is not in the maximum value set " + describeReference(maxVSUrl) + " (" + valueset.getUrl()+"), and a code from this value set is required) (code = "+value+"), (error = "+vr.getMessage()+")");
        }
      } catch (Exception e) {
        warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error "+e.getMessage()+" validating CodeableConcept using maxValueSet");
      }
    }
  }

  private String ccSummary(CodeableConcept cc) {
    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
    for (Coding c : cc.getCoding()) 
      b.append(c.getSystem()+"#"+c.getCode());
    return b.toString();
  }

  private void checkCoding(List<ValidationMessage> errors, String path, Element focus, Coding fixed) {
    checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system", focus);
    checkFixedValue(errors, path + ".code", focus.getNamedChild("code"), fixed.getCodeElement(), "code", focus);
    checkFixedValue(errors, path + ".display", focus.getNamedChild("display"), fixed.getDisplayElement(), "display", focus);
    checkFixedValue(errors, path + ".userSelected", focus.getNamedChild("userSelected"), fixed.getUserSelectedElement(), "userSelected", focus);
  }

  private void checkCoding(List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, boolean inCodeableConcept)  {
    String code = element.getNamedChildValue("code");
    String system = element.getNamedChildValue("system");
    String display = element.getNamedChildValue("display");
    rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, isAbsolute(system), "Coding.system must be an absolute reference, not a local reference");

    if (system != null && code != null && !noTerminologyChecks) {
      rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, !isValueSet(system), "The Coding references a value set, not a code system (\""+system+"\")");
      try {
        if (checkCode(errors, element, path, code, system, display))
          if (theElementCntext != null && theElementCntext.hasBinding()) {
            ElementDefinitionBindingComponent binding = theElementCntext.getBinding();
            if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, "Binding for " + path + " missing")) {
              if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) {
                ValueSet valueset = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl());
                if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(binding.getValueSet()) + " not found")) {
                  try {
                    Coding c = ObjectConverter.readAsCoding(element);
                    long t = System.nanoTime();
                    ValidationResult vr = null;
                    if (binding.getStrength() != BindingStrength.EXAMPLE) {
                      vr = context.validateCode(c, valueset);
						  }
                    txTime = txTime + (System.nanoTime() - t);
                    if (vr != null && !vr.isOk()) {
                      if (vr.IsNoService())
                        hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,  "The value provided could not be validated in the absence of a terminology server");
                      else if (vr.getErrorClass() != null && !vr.getErrorClass().isInfrastructure()) {
                        if (binding.getStrength() == BindingStrength.REQUIRED)
                          warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl()+", and a code from this value set is required)"); 
                        else if (binding.getStrength() == BindingStrength.EXTENSIBLE) {
                          if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) 
                            checkMaxValueSet(errors, path, element, profile, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), c);
                          else
                            warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code should come from this value set unless it has no suitable code)");
                        } else if (binding.getStrength() == BindingStrength.PREFERRED)
                          hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,  "Could not confirm that the codes provided are in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code is recommended to come from this value set)");
                      } else if (binding.getStrength() == BindingStrength.REQUIRED)
                        rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code is required from this value set)"+(vr.getMessage() != null ? " (error message = "+vr.getMessage()+")" : ""));
                      else if (binding.getStrength() == BindingStrength.EXTENSIBLE) {
                        if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) 
                          checkMaxValueSet(errors, path, element, profile, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), c);
                        else
                          warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The Coding provided is not in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code should come from this value set unless it has no suitable code)"+(vr.getMessage() != null ? " (error message = "+vr.getMessage()+")" : ""));
                      } else if (binding.getStrength() == BindingStrength.PREFERRED)
                        hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,  "The Coding provided is not in the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getUrl() + ", and a code is recommended to come from this value set)"+(vr.getMessage() != null ? " (error message = "+vr.getMessage()+")" : ""));
                    }
                  } catch (Exception e) {
                    warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error "+e.getMessage()+" validating Coding");
                  }
                }
              } else if (binding.hasValueSet()) {
                hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding by URI reference cannot be checked");
              } else if (!inCodeableConcept && !noBindingMsgSuppressed) {
                hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Binding for path " + path + " has no source, so can't be checked");
              }
            }
          }
      } catch (Exception e) {
        rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "Error "+e.getMessage()+" validating Coding");
      }
    }
  }

  private boolean isValueSet(String url) {
    try {
      ValueSet vs = context.fetchResourceWithException(ValueSet.class, url);
      return vs != null;
    } catch (Exception e) {
      return false;
    }
  }

  private void checkContactPoint(List<ValidationMessage> errors, String path, Element focus, ContactPoint fixed) {
    checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system", focus);
    checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value", focus);
    checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use", focus);
    checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period", focus);

  }

  protected void checkDeclaredProfiles(ResourceProfiles resourceProfiles, List<ValidationMessage> errors, Element resource, Element element, NodeStack stack) throws FHIRException {
    Element meta = element.getNamedChild("meta");
    if (meta != null) {
      List<Element> profiles = new ArrayList<Element>();
      meta.getNamedChildren("profile", profiles);
      int i = 0;
      for (Element profile : profiles) {
        String ref = profile.primitiveValue();
        String p = stack.addToLiteralPath("meta", "profile", ":" + Integer.toString(i));
        if (rule(errors, IssueType.INVALID, element.line(), element.col(), p, !Utilities.noString(ref), "StructureDefinition reference invalid")) {
          long t = System.nanoTime();
          resourceProfiles.addProfile(errors, ref, errorForUnknownProfiles, p, element);
          i++;
        }
      }
    }
  }

  private StructureDefinition checkExtension(ValidatorHostContext hostContext, List<ValidationMessage> errors, String path, Element resource, Element element, ElementDefinition def, StructureDefinition profile, NodeStack stack) throws FHIRException, IOException {
    String url = element.getNamedChildValue("url");
    boolean isModifier = element.getName().equals("modifierExtension");

    long t = System.nanoTime();
    StructureDefinition ex = context.fetchResource(StructureDefinition.class, url);
    sdTime = sdTime + (System.nanoTime() - t);
    if (ex == null) {
      if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowUnknownExtension(url), "The extension " + url + " is unknown, and not allowed here"))
        hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, isKnownExtension(url), "Unknown extension " + url);
    } else {
      if (def.getIsModifier())
        rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", ex.getSnapshot().getElement().get(0).getIsModifier(),
            "Extension modifier mismatch: the extension element is labelled as a modifier, but the underlying extension is not");
      else
        rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", !ex.getSnapshot().getElement().get(0).getIsModifier(),
            "Extension modifier mismatch: the extension element is not labelled as a modifier, but the underlying extension is");

      // two questions
      // 1. can this extension be used here?
      checkExtensionContext(errors, element, /* path+"[url='"+url+"']", */ ex, stack, ex.getUrl());

      if (isModifier)
        rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", ex.getSnapshot().getElement().get(0).getIsModifier(),
            "The Extension '" + url + "' must be used as a modifierExtension");
      else
        rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", !ex.getSnapshot().getElement().get(0).getIsModifier(),
            "The Extension '" + url + "' must not be used as an extension (it's a modifierExtension)");

      // check the type of the extension:
      Set<String> allowedTypes = listExtensionTypes(ex);
      String actualType = getExtensionType(element);
      if (actualType == null) 
        rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", allowedTypes.isEmpty(), "The Extension '" + url + "' definition is for a complex extension, so it cannot contain a value");
      else 
        rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", allowedTypes.contains(actualType), "The Extension '" + url + "' definition allows for the types "+allowedTypes.toString()+" but found type "+actualType);      
      
      // 3. is the content of the extension valid?
      validateElement(hostContext, errors, ex, ex.getSnapshot().getElement().get(0), null, null, resource, element, "Extension", stack, false);

    }
    return ex;
  }

  private String getExtensionType(Element element) {
    for (Element e : element.getChildren()) {
      if (e.getName().startsWith("value")) {
        String tn = e.getName().substring(5);
        String ltn = Utilities.uncapitalize(tn);
        if (isPrimitiveType(ltn))
          return ltn;
        else
          return tn;
      }
    }
    return null;
  }

  private Set<String> listExtensionTypes(StructureDefinition ex) {
    ElementDefinition vd = null;
    for (ElementDefinition ed : ex.getSnapshot().getElement()) {
      if (ed.getPath().startsWith("Extension.value")) {
        vd = ed;
        break;
      }
    }
    Set<String> res = new HashSet<String>();
    if (vd != null && !"0".equals(vd.getMax())) {
      for (TypeRefComponent tr : vd.getType()) {
        res.add(tr.getCode());
      }
    }
    return res;
  }

  private boolean checkExtensionContext(List<ValidationMessage> errors, Element element, StructureDefinition definition, NodeStack stack, String extensionParent) {
    String extUrl = definition.getUrl();
    CommaSeparatedStringBuilder p = new CommaSeparatedStringBuilder();
    for (String lp : stack.getLogicalPaths())
      p.append(lp);
    if (definition.getContextType() == ExtensionContext.DATATYPE) {
      boolean ok = false;
      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
      for (StringType ct : definition.getContext()) {
        b.append(ct.getValue());
        if (ct.getValue().equals("*") || stack.getLogicalPaths().contains(ct.getValue() + ".extension"))
          ok = true;
      }
      return rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), ok,
          "The extension " + extUrl + " is not allowed to be used on the logical path set [" + p.toString() + "] (allowed: datatype=" + b.toString() + ")");
    } else if (definition.getContextType() == ExtensionContext.EXTENSION) {
      boolean ok = false;
      for (StringType ct : definition.getContext())
        if (ct.getValue().equals("*") || ct.getValue().equals(extensionParent))
          ok = true;
      return rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), ok,
          "The extension " + extUrl + " is not allowed to be used with the extension '" + extensionParent + "'");
    } else if (definition.getContextType() == ExtensionContext.RESOURCE) {
      boolean ok = false;
      // String simplePath = container.getPath();
      // System.out.println(simplePath);
      // if (effetive.endsWith(".extension") || simplePath.endsWith(".modifierExtension"))
      // simplePath = simplePath.substring(0, simplePath.lastIndexOf('.'));
      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
      for (StringType ct : definition.getContext()) {
        String c = ct.getValue();
        b.append(c);
        if (c.equals("*") || stack.getLogicalPaths().contains(c + ".extension") || (c.startsWith("@") && stack.getLogicalPaths().contains(c.substring(1) + ".extension")))
          ;
        ok = true;
      }
      return rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), ok,
          "The extension " + extUrl + " is not allowed to be used on the logical path set " + p.toString() + " (allowed: resource=" + b.toString() + ")");
    } else
      throw new Error("Unknown context type");
  }
  //
  // private String simplifyPath(String path) {
  // String s = path.replace("/f:", ".");
  // while (s.contains("["))
  // s = s.substring(0, s.indexOf("["))+s.substring(s.indexOf("]")+1);
  // String[] parts = s.split("\\.");
  // int i = 0;
  // while (i < parts.length && !context.getProfiles().containsKey(parts[i].toLowerCase()))
  // i++;
  // if (i >= parts.length)
  // throw new Error("Unable to process part "+path);
  // int j = parts.length - 1;
  // while (j > 0 && (parts[j].equals("extension") || parts[j].equals("modifierExtension")))
  // j--;
  // StringBuilder b = new StringBuilder();
  // boolean first = true;
  // for (int k = i; k <= j; k++) {
  // if (k == j || !parts[k].equals(parts[k+1])) {
  // if (first)
  // first = false;
  // else
  // b.append(".");
  // b.append(parts[k]);
  // }
  // }
  // return b.toString();
  // }
  //

  private void checkFixedValue(List<ValidationMessage> errors, String path, Element focus, org.hl7.fhir.r4.model.Element fixed, String propName, Element parent) {
    if ((fixed == null || fixed.isEmpty()) && focus == null)
      ; // this is all good
    else if (fixed == null && focus != null)
      rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, false, "Unexpected element " + focus.getName());
    else if (fixed != null && !fixed.isEmpty() && focus == null)
      rule(errors, IssueType.VALUE, parent == null ? -1 : parent.line(), parent == null ? -1 : parent.col(), path, false, "Missing element '" + propName+"'");
    else {
      String value = focus.primitiveValue();
      if (fixed instanceof org.hl7.fhir.r4.model.BooleanType)
        rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r4.model.BooleanType) fixed).asStringValue(), value),
            "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r4.model.BooleanType) fixed).asStringValue() + "'");
      else if (fixed instanceof org.hl7.fhir.r4.model.IntegerType)
        rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r4.model.IntegerType) fixed).asStringValue(), value),
            "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r4.model.IntegerType) fixed).asStringValue() + "'");
      else if (fixed instanceof org.hl7.fhir.r4.model.DecimalType)
        rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r4.model.DecimalType) fixed).asStringValue(), value),
            "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r4.model.DecimalType) fixed).asStringValue() + "'");
      else if (fixed instanceof org.hl7.fhir.r4.model.Base64BinaryType)
        rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r4.model.Base64BinaryType) fixed).asStringValue(), value),
            "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r4.model.Base64BinaryType) fixed).asStringValue() + "'");
      else if (fixed instanceof org.hl7.fhir.r4.model.InstantType)
        rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r4.model.InstantType) fixed).getValue().toString(), value),
            "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r4.model.InstantType) fixed).asStringValue() + "'");
      else if (fixed instanceof org.hl7.fhir.r4.model.StringType)
        rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r4.model.StringType) fixed).getValue(), value),
            "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r4.model.StringType) fixed).getValue() + "'");
      else if (fixed instanceof org.hl7.fhir.r4.model.UriType)
        rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r4.model.UriType) fixed).getValue(), value),
            "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r4.model.UriType) fixed).getValue() + "'");
      else if (fixed instanceof org.hl7.fhir.r4.model.DateType)
        rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r4.model.DateType) fixed).getValue().toString(), value),
            "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r4.model.DateType) fixed).getValue() + "'");
      else if (fixed instanceof org.hl7.fhir.r4.model.DateTimeType)
        rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r4.model.DateTimeType) fixed).getValue().toString(), value),
            "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r4.model.DateTimeType) fixed).getValue() + "'");
      else if (fixed instanceof org.hl7.fhir.r4.model.OidType)
        rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r4.model.OidType) fixed).getValue(), value),
            "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r4.model.OidType) fixed).getValue() + "'");
      else if (fixed instanceof org.hl7.fhir.r4.model.UuidType)
        rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r4.model.UuidType) fixed).getValue(), value),
            "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r4.model.UuidType) fixed).getValue() + "'");
      else if (fixed instanceof org.hl7.fhir.r4.model.CodeType)
        rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r4.model.CodeType) fixed).getValue(), value),
            "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r4.model.CodeType) fixed).getValue() + "'");
      else if (fixed instanceof org.hl7.fhir.r4.model.IdType)
        rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, check(((org.hl7.fhir.r4.model.IdType) fixed).getValue(), value),
            "Value is '" + value + "' but must be '" + ((org.hl7.fhir.r4.model.IdType) fixed).getValue() + "'");
      else if (fixed instanceof Quantity)
        checkQuantity(errors, path, focus, (Quantity) fixed);
      else if (fixed instanceof Address)
        checkAddress(errors, path, focus, (Address) fixed);
      else if (fixed instanceof ContactPoint)
        checkContactPoint(errors, path, focus, (ContactPoint) fixed);
      else if (fixed instanceof Attachment)
        checkAttachment(errors, path, focus, (Attachment) fixed);
      else if (fixed instanceof Identifier)
        checkIdentifier(errors, path, focus, (Identifier) fixed);
      else if (fixed instanceof Coding)
        checkCoding(errors, path, focus, (Coding) fixed);
      else if (fixed instanceof HumanName)
        checkHumanName(errors, path, focus, (HumanName) fixed);
      else if (fixed instanceof CodeableConcept)
        checkCodeableConcept(errors, path, focus, (CodeableConcept) fixed);
      else if (fixed instanceof Timing)
        checkTiming(errors, path, focus, (Timing) fixed);
      else if (fixed instanceof Period)
        checkPeriod(errors, path, focus, (Period) fixed);
      else if (fixed instanceof Range)
        checkRange(errors, path, focus, (Range) fixed);
      else if (fixed instanceof Ratio)
        checkRatio(errors, path, focus, (Ratio) fixed);
      else if (fixed instanceof SampledData)
        checkSampledData(errors, path, focus, (SampledData) fixed);

      else
        rule(errors, IssueType.EXCEPTION, focus.line(), focus.col(), path, false, "Unhandled fixed value type " + fixed.getClass().getName());
      List<Element> extensions = new ArrayList<Element>();
      focus.getNamedChildren("extension", extensions);
      if (fixed.getExtension().size() == 0) {
        rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, extensions.size() == 0, "No extensions allowed");
      } else if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, extensions.size() == fixed.getExtension().size(),
          "Extensions count mismatch: expected " + Integer.toString(fixed.getExtension().size()) + " but found " + Integer.toString(extensions.size()))) {
        for (Extension e : fixed.getExtension()) {
          Element ex = getExtensionByUrl(extensions, e.getUrl());
          if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, ex != null, "Extension count mismatch: unable to find extension: " + e.getUrl())) {
            checkFixedValue(errors, path, ex.getNamedChild("extension").getNamedChild("value"), e.getValue(), "extension.value", ex.getNamedChild("extension"));
          }
        }
      }
    }
  }

  private void checkHumanName(List<ValidationMessage> errors, String path, Element focus, HumanName fixed) {
    checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use", focus);
    checkFixedValue(errors, path + ".text", focus.getNamedChild("text"), fixed.getTextElement(), "text", focus);
    checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period", focus);

    List<Element> parts = new ArrayList<Element>();
    focus.getNamedChildren("family", parts);
    if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() > 0 == fixed.hasFamily(),
        "Expected " + (fixed.hasFamily() ? "1" : "0") + " but found " + Integer.toString(parts.size()) + " family elements")) {
      for (int i = 0; i < parts.size(); i++)
        checkFixedValue(errors, path + ".family", parts.get(i), fixed.getFamilyElement(), "family", focus);
    }
    focus.getNamedChildren("given", parts);
    if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getGiven().size(),
        "Expected " + Integer.toString(fixed.getGiven().size()) + " but found " + Integer.toString(parts.size()) + " given elements")) {
      for (int i = 0; i < parts.size(); i++)
        checkFixedValue(errors, path + ".given", parts.get(i), fixed.getGiven().get(i), "given", focus);
    }
    focus.getNamedChildren("prefix", parts);
    if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getPrefix().size(),
        "Expected " + Integer.toString(fixed.getPrefix().size()) + " but found " + Integer.toString(parts.size()) + " prefix elements")) {
      for (int i = 0; i < parts.size(); i++)
        checkFixedValue(errors, path + ".prefix", parts.get(i), fixed.getPrefix().get(i), "prefix", focus);
    }
    focus.getNamedChildren("suffix", parts);
    if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, parts.size() == fixed.getSuffix().size(),
        "Expected " + Integer.toString(fixed.getSuffix().size()) + " but found " + Integer.toString(parts.size()) + " suffix elements")) {
      for (int i = 0; i < parts.size(); i++)
        checkFixedValue(errors, path + ".suffix", parts.get(i), fixed.getSuffix().get(i), "suffix", focus);
    }
  }

  private void checkIdentifier(List<ValidationMessage> errors, String path, Element element, ElementDefinition context) {
    String system = element.getNamedChildValue("system");
    rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, isAbsolute(system), "Identifier.system must be an absolute reference, not a local reference");
  }

  private void checkIdentifier(List<ValidationMessage> errors, String path, Element focus, Identifier fixed) {
    checkFixedValue(errors, path + ".use", focus.getNamedChild("use"), fixed.getUseElement(), "use", focus);
    checkFixedValue(errors, path + ".type", focus.getNamedChild("type"), fixed.getType(), "type", focus);
    checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system", focus);
    checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value", focus);
    checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriod(), "period", focus);
    checkFixedValue(errors, path + ".assigner", focus.getNamedChild("assigner"), fixed.getAssigner(), "assigner", focus);
  }

  private void checkPeriod(List<ValidationMessage> errors, String path, Element focus, Period fixed) {
    checkFixedValue(errors, path + ".start", focus.getNamedChild("start"), fixed.getStartElement(), "start", focus);
    checkFixedValue(errors, path + ".end", focus.getNamedChild("end"), fixed.getEndElement(), "end", focus);
  }

  private void checkPrimitive(Object appContext, List<ValidationMessage> errors, String path, String type, ElementDefinition context, Element e, StructureDefinition profile) throws FHIRException, IOException {
    if (isBlank(e.primitiveValue())) {
      rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.hasChildren(), "primitive types must have a value or must have child extensions");
      return;
    }
    String regex = context.getExtensionString(ToolingExtensions.EXT_REGEX);
    if (regex!=null)
      regex = context.getExtensionString("http://hl7.org/fhir/StructureDefinition/regex");
    if (regex!=null)
        rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().matches(regex), "Element value '" + e.primitiveValue() + "' does not meet regex '" + regex + "'");
    
    if (type.equals("boolean")) { 
      rule(errors, IssueType.INVALID, e.line(), e.col(), path, "true".equals(e.primitiveValue()) || "false".equals(e.primitiveValue()), "boolean values must be 'true' or 'false'");
    }
    if (type.equals("uri")) {
      rule(errors, IssueType.INVALID, e.line(), e.col(), path, !e.primitiveValue().startsWith("oid:"), "URI values cannot start with oid:");
      rule(errors, IssueType.INVALID, e.line(), e.col(), path, !e.primitiveValue().startsWith("uuid:"), "URI values cannot start with uuid:");
      rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().equals(e.primitiveValue().trim()), "URI values cannot have leading or trailing whitespace");
      rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength()==0 ||  e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum length of " + context.getMaxLength());
      
      // now, do we check the URI target? 
      if (fetcher != null) {
        rule(errors, IssueType.INVALID, e.line(), e.col(), path, fetcher.resolveURL(appContext, path, e.primitiveValue()), "URL value '"+e.primitiveValue()+"' does not resolve");
      }
    }
    if (type.equalsIgnoreCase("string") && e.hasPrimitiveValue()) {
      if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue() == null || e.primitiveValue().length() > 0, "@value cannot be empty")) {
        warning(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue() == null || e.primitiveValue().trim().equals(e.primitiveValue()), "value should not start or finish with whitespace");
        rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength()==0 ||  e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum length of " + context.getMaxLength());
      }
    }
    if (type.equals("dateTime")) {
      rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()), "The value '" + e.primitiveValue() + "' does not have a valid year");
      rule(errors, IssueType.INVALID, e.line(), e.col(), path,
          e.primitiveValue()
          .matches("-?[0-9]{4}(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?)?)?)?"),
          "Not a valid date time");
      rule(errors, IssueType.INVALID, e.line(), e.col(), path, !hasTime(e.primitiveValue()) || hasTimeZone(e.primitiveValue()), "if a date has a time, it must have a timezone");
      rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength()==0 ||  e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum length of " + context.getMaxLength());
    }
    if (type.equals("date")) {
        rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()), "The value '" + e.primitiveValue() + "' does not have a valid year");
        rule(errors, IssueType.INVALID, e.line(), e.col(), path, e.primitiveValue().matches("-?[0-9]{4}(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?"),
            "Not a valid date");
        rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength()==0 ||  e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum value of " + context.getMaxLength());
    }
    if (type.equals("integer")) {
      rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxValueIntegerType() || !context.getMaxValueIntegerType().hasValue() || (context.getMaxValueIntegerType().getValue() >= new Integer(e.getValue()).intValue()), "value is greater than permitted maximum value of " + (context.hasMaxValueIntegerType() ? context.getMaxValueIntegerType() : ""));
      rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMinValueIntegerType() || !context.getMinValueIntegerType().hasValue() || (context.getMinValueIntegerType().getValue() <= new Integer(e.getValue()).intValue()), "value is less than permitted minimum value of " + (context.hasMinValueIntegerType() ? context.getMinValueIntegerType() : ""));
    }
    if (type.equals("instant")) {
      rule(errors, IssueType.INVALID, e.line(), e.col(), path,
          e.primitiveValue().matches("-?[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))"),
          "The instant '" + e.primitiveValue() + "' is not valid (by regex)");
      rule(errors, IssueType.INVALID, e.line(), e.col(), path, yearIsValid(e.primitiveValue()), "The value '" + e.primitiveValue() + "' does not have a valid year");
    }

    if (type.equals("code") && e.primitiveValue() != null) {
      // Technically, a code is restricted to string which has at least one character and no leading or trailing whitespace, and where there is no whitespace
      // other than single spaces in the contents
      rule(errors, IssueType.INVALID, e.line(), e.col(), path, passesCodeWhitespaceRules(e.primitiveValue()), "The code '" + e.primitiveValue() + "' is not valid (whitespace rules)");
      rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength()==0 ||  e.primitiveValue().length() <= context.getMaxLength(), "value is longer than permitted maximum length of " + context.getMaxLength());
    }

    if (context.hasBinding() && e.primitiveValue() != null) {
      checkPrimitiveBinding(errors, path, type, context, e, profile);
    }

    if (type.equals("xhtml")) {
      XhtmlNode xhtml = e.getXhtml();
      if (xhtml != null) { // if it is null, this is an error already noted in the parsers
        // check that the namespace is there and correct.
        String ns = xhtml.getNsDecl();
        rule(errors, IssueType.INVALID, e.line(), e.col(), path, FormatUtilities.XHTML_NS.equals(ns), "Wrong namespace on the XHTML ('"+ns+"')");
        // check that inner namespaces are all correct 
        checkInnerNS(errors, e, path, xhtml.getChildNodes());
        rule(errors, IssueType.INVALID, e.line(), e.col(), path, "div".equals(xhtml.getName()), "Wrong name on the XHTML ('"+ns+"') - must start with div");
        // check that no illegal elements and attributes have been used
        checkInnerNames(errors, e, path, xhtml.getChildNodes());
      } 
    }

    if (context.hasFixed())
      checkFixedValue(errors,path,e, context.getFixed(), context.getSliceName(), null);

    // for nothing to check
  }

  private void checkInnerNames(List<ValidationMessage> errors, Element e, String path, List<XhtmlNode> list) {
    for (XhtmlNode node : list) {
      if (node.getNodeType() == NodeType.Element) {
        rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.existsInList(node.getName(), 
            "p", "br", "div", "h1", "h2", "h3", "h4", "h5", "h6", "a", "span", "b", "em", "i", "strong",
            "small", "big", "tt", "small", "dfn", "q", "var", "abbr", "acronym", "cite", "blockquote", "hr", "address", "bdo", "kbd", "q", "sub", "sup",
            "ul", "ol", "li", "dl", "dt", "dd", "pre", "table", "caption", "colgroup", "col", "thead", "tr", "tfoot", "tbody", "th", "td",
            "code", "samp", "img", "map", "area"

            ), "Illegal element name in the XHTML ('"+node.getName()+"')");
        for (String an : node.getAttributes().keySet()) {
          boolean ok = an.startsWith("xmlns") || Utilities.existsInList(an, 
              "title", "style", "class", "id", "lang", "xml:lang", "dir", "accesskey", "tabindex",
              // tables
              "span", "width", "align", "valign", "char", "charoff", "abbr", "axis", "headers", "scope", "rowspan", "colspan") ||

              Utilities.existsInList(node.getName()+"."+an, "a.href", "a.name", "img.src", "img.border", "div.xmlns", "blockquote.cite", "q.cite",
                  "a.charset", "a.type", "a.name", "a.href", "a.hreflang", "a.rel", "a.rev", "a.shape", "a.coords", "img.src",
                  "img.alt", "img.longdesc", "img.height", "img.width", "img.usemap", "img.ismap", "map.name", "area.shape",
                  "area.coords", "area.href", "area.nohref", "area.alt", "table.summary", "table.width", "table.border",
                  "table.frame", "table.rules", "table.cellspacing", "table.cellpadding", "pre.space"
                  );
          if (!ok)
            rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, "Illegal attribute name in the XHTML ('"+an+"' on '"+node.getName()+"')");
        }
        checkInnerNames(errors, e, path, node.getChildNodes());
      }
    }	
  }

  private void checkInnerNS(List<ValidationMessage> errors, Element e, String path, List<XhtmlNode> list) {
    for (XhtmlNode node : list) {
      if (node.getNodeType() == NodeType.Element) {
        String ns = node.getNsDecl();
        rule(errors, IssueType.INVALID, e.line(), e.col(), path, ns == null || FormatUtilities.XHTML_NS.equals(ns), "Wrong namespace on the XHTML ('"+ns+"')");
        checkInnerNS(errors, e, path, node.getChildNodes());
      }
    }	
  }

  private void checkPrimitiveBinding(List<ValidationMessage> errors, String path, String type, ElementDefinition elementContext, Element element, StructureDefinition profile) {
    // We ignore bindings that aren't on string, uri or code
    if (!element.hasPrimitiveValue() || !("code".equals(type) || "string".equals(type) || "uri".equals(type))) {
      return;
    }
    if (noTerminologyChecks)
      return;

    String value = element.primitiveValue();
    // System.out.println("check "+value+" in "+path);

    // firstly, resolve the value set
    ElementDefinitionBindingComponent binding = elementContext.getBinding();
    if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) {
      ValueSet vs = resolveBindingReference(profile, binding.getValueSet(), profile.getUrl());
      if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "ValueSet {0} not found", describeReference(binding.getValueSet()))) {
        long t = System.nanoTime();
        ValidationResult vr = null;
        if (binding.getStrength() != BindingStrength.EXAMPLE) {
			  vr = context.validateCode(null, value, null, vs);
		  }
		  txTime = txTime + (System.nanoTime() - t);
        if (vr != null && !vr.isOk()) {
          if (vr.IsNoService())
            hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,  "The value provided ('"+value+"') could not be validated in the absence of a terminology server");
          else if (binding.getStrength() == BindingStrength.REQUIRED)
            rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The value provided ('"+value+"') is not in the value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ", and a code is required from this value set)"+(vr.getMessage() != null ? " (error message = "+vr.getMessage()+")" : ""));
          else if (binding.getStrength() == BindingStrength.EXTENSIBLE) {
            if (binding.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet")) 
              checkMaxValueSet(errors, path, element, profile, (Reference) binding.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet").get(0).getValue(), value);
            else
              warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false, "The value provided ('"+value+"') is not in the value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ", and a code should come from this value set unless it has no suitable code)"+(vr.getMessage() != null ? " (error message = "+vr.getMessage()+")" : ""));
          } else if (binding.getStrength() == BindingStrength.PREFERRED)
            hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, false,  "The value provided ('"+value+"') is not in the value set " + describeReference(binding.getValueSet()) + " (" + vs.getUrl() + ", and a code is recommended to come from this value set)"+(vr.getMessage() != null ? " (error message = "+vr.getMessage()+")" : ""));
        }
      }
    } else if (!noBindingMsgSuppressed)
      hint(errors, IssueType.CODEINVALID, element.line(), element.col(), path, !type.equals("code"), "Binding has no source, so can't be checked");
  }

  private void checkQuantity(List<ValidationMessage> errors, String path, Element focus, Quantity fixed) {
    checkFixedValue(errors, path + ".value", focus.getNamedChild("value"), fixed.getValueElement(), "value", focus);
    checkFixedValue(errors, path + ".comparator", focus.getNamedChild("comparator"), fixed.getComparatorElement(), "comparator", focus);
    checkFixedValue(errors, path + ".units", focus.getNamedChild("unit"), fixed.getUnitElement(), "units", focus);
    checkFixedValue(errors, path + ".system", focus.getNamedChild("system"), fixed.getSystemElement(), "system", focus);
    checkFixedValue(errors, path + ".code", focus.getNamedChild("code"), fixed.getCodeElement(), "code", focus);
  }

  // implementation

  private void checkRange(List<ValidationMessage> errors, String path, Element focus, Range fixed) {
    checkFixedValue(errors, path + ".low", focus.getNamedChild("low"), fixed.getLow(), "low", focus);
    checkFixedValue(errors, path + ".high", focus.getNamedChild("high"), fixed.getHigh(), "high", focus);

  }

  private void checkRatio(List<ValidationMessage> errors, String path, Element focus, Ratio fixed) {
    checkFixedValue(errors, path + ".numerator", focus.getNamedChild("numerator"), fixed.getNumerator(), "numerator", focus);
    checkFixedValue(errors, path + ".denominator", focus.getNamedChild("denominator"), fixed.getDenominator(), "denominator", focus);
  }

  private void checkReference(ValidatorHostContext hostContext, List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, ElementDefinition container, String parentType, NodeStack stack) throws FHIRException, IOException {
    Reference reference = ObjectConverter.readAsReference(element);
    
    String ref = reference.getReference();
    if (Utilities.noString(ref)) {
      if (Utilities.noString(reference.getIdentifier().getSystem()) && Utilities.noString(reference.getIdentifier().getValue())) {
        warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, !Utilities.noString(element.getNamedChildValue("display")), "A Reference without an actual reference or identifier should have a display");
      }
      return;
    }

    Element we = localResolve(ref, stack, errors, path);
    String refType;
    if (ref.startsWith("#")) {
      refType = "contained";
    } else {
      if (we == null) {
        refType = "remote";
      } else {
        refType = "bundle";
      }
    }
    ReferenceValidationPolicy pol = refType.equals("contained") || refType.equals("bundle") ? ReferenceValidationPolicy.CHECK_VALID : fetcher == null ? ReferenceValidationPolicy.IGNORE : fetcher.validationPolicy(hostContext.appContext, path, ref);

    if (pol.checkExists()) {
      if (we == null) {
        if (fetcher == null)
          throw new FHIRException("Resource resolution services not provided");
        we = fetcher.fetch(hostContext.appContext, ref);
      }
      rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, we != null, "Unable to resolve resource '"+ref+"'");
    }

    String ft;
    if (we != null)
      ft = we.getType();
    else
      ft = tryParse(ref);

    if (we != null && pol.checkType()) {
      if (warning(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ft!=null, "Unable to determine type of target resource")) {
        boolean ok = false;
        CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
        for (TypeRefComponent type : container.getType()) {
          if (!ok && type.getCode().equals("Reference")) {
            // we validate as much as we can. First, can we infer a type from the profile? 
            if (!type.hasTargetProfile() || type.getTargetProfile().equals("http://hl7.org/fhir/StructureDefinition/Resource"))
              ok = true;
            else {
              String pr = type.getTargetProfile();

              String bt = getBaseType(profile, pr);
              StructureDefinition sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + bt);
              if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, bt != null, "Unable to resolve the profile reference '" + pr + "'")) {
                b.append(bt);
                ok = bt.equals(ft);
                if (ok && we!=null && pol.checkValid()) {
                  doResourceProfile(hostContext, we, pr, errors, stack.push(we, -1, null, null), path, element);
                }
              } else
                ok = true; // suppress following check
              if (ok && type.hasAggregation()) {
                boolean modeOk;
                for (Enumeration<AggregationMode> mode : type.getAggregation()) {
                  if (mode.getValue().equals(AggregationMode.CONTAINED) && refType.equals("contained"))
                    ok = true;
                  else if (mode.getValue().equals(AggregationMode.BUNDLED) && refType.equals("bundled"))
                    ok = true;
                  else if (mode.getValue().equals(AggregationMode.REFERENCED) && (refType.equals("bundled")||refType.equals("remote")))
                    ok = true;
                }
                rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ok, "Reference is " + refType + " which isn't supported by the specified aggregation mode(s) for the reference");
              }
            }
          }
          if (!ok && type.getCode().equals("*")) {
            ok = true; // can refer to anything
          }
        }
        rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, ok, "Invalid Resource target type. Found " + ft + ", but expected one of (" + b.toString() + ")");
      }
    }
    if (pol == ReferenceValidationPolicy.CHECK_VALID) {
      // todo....
    }
  }
  
  private void doResourceProfile(ValidatorHostContext hostContext, Element resource, String profile, List<ValidationMessage> errors, NodeStack stack, String path, Element element) throws FHIRException, IOException {
    ResourceProfiles resourceProfiles = addResourceProfile(errors, resource, profile, path, element, stack);
    if (resourceProfiles.isProcessed()) {
      start(hostContext, errors, resource, resource, null, stack);  
    }
  }

  private ResourceProfiles getResourceProfiles(Element resource, NodeStack stack) {
    ResourceProfiles resourceProfiles = resourceProfilesMap.get(resource);
    if (resourceProfiles==null) {
      resourceProfiles = new ResourceProfiles(resource, stack);
      resourceProfilesMap.put(resource, resourceProfiles);
    }
    return resourceProfiles;
  }

  private ResourceProfiles addResourceProfile(List<ValidationMessage> errors, Element resource, String profile, String path, Element element, NodeStack stack) {
    ResourceProfiles resourceProfiles = getResourceProfiles(resource, stack);
    resourceProfiles.addProfile(errors, profile, errorForUnknownProfiles, path, element);
    return resourceProfiles;
  }

  private String checkResourceType(String type)  {
    long t = System.nanoTime();
    try {
      if (context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + type) != null)
        return type;
      else
        return null;
    } finally {
      sdTime = sdTime + (System.nanoTime() - t);
    }
  }

  private void checkSampledData(List<ValidationMessage> errors, String path, Element focus, SampledData fixed) {
    checkFixedValue(errors, path + ".origin", focus.getNamedChild("origin"), fixed.getOrigin(), "origin", focus);
    checkFixedValue(errors, path + ".period", focus.getNamedChild("period"), fixed.getPeriodElement(), "period", focus);
    checkFixedValue(errors, path + ".factor", focus.getNamedChild("factor"), fixed.getFactorElement(), "factor", focus);
    checkFixedValue(errors, path + ".lowerLimit", focus.getNamedChild("lowerLimit"), fixed.getLowerLimitElement(), "lowerLimit", focus);
    checkFixedValue(errors, path + ".upperLimit", focus.getNamedChild("upperLimit"), fixed.getUpperLimitElement(), "upperLimit", focus);
    checkFixedValue(errors, path + ".dimensions", focus.getNamedChild("dimensions"), fixed.getDimensionsElement(), "dimensions", focus);
    checkFixedValue(errors, path + ".data", focus.getNamedChild("data"), fixed.getDataElement(), "data", focus);
  }

  private void checkTiming(List<ValidationMessage> errors, String path, Element focus, Timing fixed) {
    checkFixedValue(errors, path + ".repeat", focus.getNamedChild("repeat"), fixed.getRepeat(), "value", focus);

    List<Element> events = new ArrayList<Element>();
    focus.getNamedChildren("event", events);
    if (rule(errors, IssueType.VALUE, focus.line(), focus.col(), path, events.size() == fixed.getEvent().size(),
        "Expected " + Integer.toString(fixed.getEvent().size()) + " but found " + Integer.toString(events.size()) + " event elements")) {
      for (int i = 0; i < events.size(); i++)
        checkFixedValue(errors, path + ".event", events.get(i), fixed.getEvent().get(i), "event", focus);
    }
  }

  private boolean codeinExpansion(ValueSetExpansionContainsComponent cnt, String system, String code) {
    for (ValueSetExpansionContainsComponent c : cnt.getContains()) {
      if (code.equals(c.getCode()) && system.equals(c.getSystem().toString()))
        return true;
      if (codeinExpansion(c, system, code))
        return true;
    }    
    return false;
  }

  private boolean codeInExpansion(ValueSet vs, String system, String code) {
    for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) {
      if (code.equals(c.getCode()) && (system == null || system.equals(c.getSystem())))
        return true;
      if (codeinExpansion(c, system, code))
        return true;
    }
    return false;
  }

  private String describeReference(Type reference) {
    if (reference == null)
      return "null";
    if (reference instanceof UriType)
      return ((UriType) reference).getValue();
    if (reference instanceof Reference)
      return ((Reference) reference).getReference();
    return "??";
  }

  private String describeTypes(List<TypeRefComponent> types) {
    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
    for (TypeRefComponent t : types) {
      b.append(t.getCode());
    }
    return b.toString();
  }

  protected ElementDefinition findElement(StructureDefinition profile, String name) {
    for (ElementDefinition c : profile.getSnapshot().getElement()) {
      if (c.getPath().equals(name)) {
        return c;
      }
    }
    return null;
  }

  public BestPracticeWarningLevel getBasePracticeWarningLevel() {
    return bpWarnings;
  }  

  private String getBaseType(StructureDefinition profile, String pr)  {
    StructureDefinition p = resolveProfile(profile, pr);
    if (p == null)
      return null;
    else 
      return p.getType();
  }

  @Override
  public CheckDisplayOption getCheckDisplay() {
    return checkDisplay;
  }

  //	private String findProfileTag(Element element) {
  //  	String uri = null;
  //	  List<Element> list = new ArrayList<Element>();
  //	  element.getNamedChildren("category", list);
  //	  for (Element c : list) {
  //	  	if ("http://hl7.org/fhir/tag/profile".equals(c.getAttribute("scheme"))) {
  //	  		uri = c.getAttribute("term");
  //	  	}
  //	  }
  //	  return uri;
  //  }

  private ConceptDefinitionComponent getCodeDefinition(ConceptDefinitionComponent c, String code) {
    if (code.equals(c.getCode()))
      return c;
    for (ConceptDefinitionComponent g : c.getConcept()) {
      ConceptDefinitionComponent r = getCodeDefinition(g, code);
      if (r != null)
        return r;
    }
    return null;
  }

  private ConceptDefinitionComponent getCodeDefinition(CodeSystem cs, String code) {
    for (ConceptDefinitionComponent c : cs.getConcept()) {
      ConceptDefinitionComponent r = getCodeDefinition(c, code);
      if (r != null)
        return r;
    }
    return null;
  }

  private Element getContainedById(Element container, String id) {
    List<Element> contained = new ArrayList<Element>();
    container.getNamedChildren("contained", contained);
    for (Element we : contained) {
      if (id.equals(we.getNamedChildValue("id")))
        return we;
    }   
    return null;
  }

  public IWorkerContext getContext() {
    return context;
  }

  private ElementDefinition getCriteriaForDiscriminator(String path, ElementDefinition element, String discriminator, StructureDefinition profile) throws DefinitionException, FHIRLexerException {
    if ("value".equals(discriminator) && element.hasFixed())
      return element;
    
    ExpressionNode expr = fpe.parse(discriminator);
    long t2 = System.nanoTime();
    ElementDefinition ed = fpe.evaluateDefinition(expr, profile, element);
    sdTime = sdTime + (System.nanoTime() - t2);
    return ed;
  }

 
  private Element getExtensionByUrl(List<Element> extensions, String urlSimple) {
    for (Element e : extensions) {
      if (urlSimple.equals(e.getNamedChildValue("url")))
        return e;
    }
    return null;
  }

  public List<String> getExtensionDomains() {
    return extensionDomains;
  }

  private Element getFromBundle(Element bundle, String ref, String fullUrl, List<ValidationMessage> errors, String path) {
    String targetUrl = null;
    String version = "";
    if (ref.startsWith("http") || ref.startsWith("urn")) {
      // We've got an absolute reference, no need to calculate
      if (ref.contains("/_history/")) {
        targetUrl = ref.substring(0, ref.indexOf("/_history/") - 1);
        version = ref.substring(ref.indexOf("/_history/") + 10);
      }  else
        targetUrl = ref;

    } else if (fullUrl == null) {
      //This isn't a problem for signatures - if it's a signature, we won't have a resolution for a relative reference.  For anything else, this is an error
      rule(errors, IssueType.REQUIRED, -1, -1, path, path.startsWith("Bundle.signature"), "Relative Reference appears inside Bundle whose entry is missing a fullUrl");
      return null;

    } else if (ref.split("/").length!=2) {
      rule(errors, IssueType.INVALID, -1, -1, path, false, "Relative URLs must be of the format [ResourceName]/[id].  Encountered " + ref);
      return null;

    } else {
      String base = "";
      if (fullUrl.startsWith("urn")) {
        String[] parts = fullUrl.split("\\:");
        for (int i=0; i < parts.length-1; i++) {
          base = base + parts[i] + ":";
        }
      } else {
        String[] parts;
        parts = fullUrl.split("/");
        for (int i=0; i < parts.length-2; i++) {
          base = base + parts[i] + "/";
        }
      }

      String id = null;
      if (ref.contains("/_history/")) {
        version = ref.substring(ref.indexOf("/_history/") + 10);
        id = ref.substring(0, ref.indexOf("/history/")-1);
      } else if (base.startsWith("urn"))
        id = ref.split("/")[1];
      else
        id = ref;

      targetUrl = base + id;
    }

    List<Element> entries = new ArrayList<Element>();
    bundle.getNamedChildren("entry", entries);
    Element match = null;
    for (Element we : entries) {
      if (we.getChildValue("fullUrl").equals(targetUrl)) {
        Element r = we.getNamedChild("resource");
        if (version.isEmpty()) {
          rule(errors, IssueType.FORBIDDEN, -1, -1, path, match==null, "Multiple matches in bundle for reference " + ref);
          match = r;
        } else {
          try {
            if (r.getChildren("meta").get(0).getChildValue("versionId").equals(version)) {
              rule(errors, IssueType.FORBIDDEN, -1, -1, path, match==null, "Multiple matches in bundle for reference " + ref);
              match = r;
            }
          } catch (Exception e) {
            warning(errors, IssueType.REQUIRED, -1, -1, path, r.getChildren("meta").size()==1 && r.getChildren("meta").get(0).getChildValue("versionId")!=null, "Entries matching fullURL " + targetUrl + " should declare meta/versionId because there are version-specific references");
            // If one of these things is null
          }
        }
      }
    }

    warning(errors, IssueType.REQUIRED, -1, -1, path, match!=null || !targetUrl.startsWith("urn"), "URN reference is not locally contained within the bundle " + ref);
    return match;
  }  

  private StructureDefinition getProfileForType(String type) {
    if (logical != null)
      for (BundleEntryComponent be : logical.getEntry()) {
        if (be.hasResource() && be.getResource() instanceof StructureDefinition) {
          StructureDefinition sd = (StructureDefinition) be.getResource();
          if (sd.getId().equals(type))
            return sd;
        }
      }

    long t = System.nanoTime();
    try {
      return context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + type);
    } finally {
      sdTime = sdTime + (System.nanoTime() - t);
    }
  }

  private Element getValueForDiscriminator(Object appContext, List<ValidationMessage> errors, Element element, String discriminator, ElementDefinition criteria, NodeStack stack) throws FHIRException, IOException  {
    String p = stack.getLiteralPath()+"."+element.getName();
    Element focus = element;
    String[] dlist = discriminator.split("\\.");
    for (String d : dlist) {
      if (focus.fhirType().equals("Reference") && d.equals("reference")) {
        String url = focus.getChildValue("reference");
        if (Utilities.noString(url))
          throw new FHIRException("No reference resolving discriminator "+discriminator+" from "+element.getProperty().getName());
        // Note that we use the passed in stack here. This might be a problem if the discriminator is deep enough? 
        Element target = resolve(appContext, url, stack, errors, p);
        if (target == null)
          throw new FHIRException("Unable to find resource "+url+" at "+d+" resolving discriminator "+discriminator+" from "+element.getProperty().getName());
        focus = target;
      } else if (d.equals("value") && focus.isPrimitive()) {
        return focus;
      } else {
        List<Element> children = focus.getChildren(d);
        if (children.isEmpty())
          throw new FHIRException("Unable to find "+d+" resolving discriminator "+discriminator+" from "+element.getProperty().getName());
        if (children.size() > 1)
          throw new FHIRException("Found "+Integer.toString(children.size())+" items for "+d+" resolving discriminator "+discriminator+" from "+element.getProperty().getName());
        focus = children.get(0);
        p = p + "."+d;
      }
    }
    return focus;
  }

  private CodeSystem getCodeSystem(String system) {
    long t = System.nanoTime();
    try {
      return context.fetchCodeSystem(system);
    } finally {
      txTime = txTime + (System.nanoTime() - t);
    }
  }

  private boolean hasTime(String fmt) {
    return fmt.contains("T");
  }

  private boolean hasTimeZone(String fmt) {
    return fmt.length() > 10 && (fmt.substring(10).contains("-") || fmt.substring(10).contains("+") || fmt.substring(10).contains("Z"));
  }

  private boolean isAbsolute(String uri) {
    return Utilities.noString(uri) || uri.startsWith("http:") || uri.startsWith("https:") || uri.startsWith("urn:uuid:") || uri.startsWith("urn:oid:") || uri.startsWith("urn:ietf:")
        || uri.startsWith("urn:iso:") || uri.startsWith("urn:iso-astm:") || isValidFHIRUrn(uri);
  }

  private boolean isValidFHIRUrn(String uri) {
    return (uri.equals("urn:x-fhir:uk:id:nhs-number")) || uri.startsWith("urn:"); // Anyone can invent a URN, so why should we complain?
  }

  public boolean isAnyExtensionsAllowed() {
    return anyExtensionsAllowed;
  }

  public boolean isErrorForUnknownProfiles() {
    return errorForUnknownProfiles;
  }

  public void setErrorForUnknownProfiles(boolean errorForUnknownProfiles) {
    this.errorForUnknownProfiles = errorForUnknownProfiles;
  }

  private boolean isParametersEntry(String path) {
    String[] parts = path.split("\\.");
    return parts.length > 2 && parts[parts.length - 1].equals("resource") && (pathEntryHasName(parts[parts.length - 2], "parameter") || pathEntryHasName(parts[parts.length - 2], "part"));
  }

  private boolean isBundleEntry(String path) {
    String[] parts = path.split("\\.");
    return parts.length > 2 && parts[parts.length - 1].equals("resource") && pathEntryHasName(parts[parts.length - 2], "entry");
  }

  private boolean isBundleOutcome(String path) {
    String[] parts = path.split("\\.");
    return parts.length > 2 && parts[parts.length - 1].equals("outcome") && pathEntryHasName(parts[parts.length - 2], "response");
  }


  private static boolean pathEntryHasName(String thePathEntry, String theName) {
    if (thePathEntry.equals(theName)) {
      return true;
    }
    if (thePathEntry.length() >= theName.length() + 3) {
      if (thePathEntry.startsWith(theName)) {
        if (thePathEntry.charAt(theName.length()) == '[') {
          return true;
        }
      }
    }
    return false;
  }

  private boolean isPrimitiveType(String code) {
    StructureDefinition sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+code);
    return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE;
  }



  public boolean isSuppressLoincSnomedMessages() {
    return suppressLoincSnomedMessages;
  }

  private boolean nameMatches(String name, String tail) {
    if (tail.endsWith("[x]"))
      return name.startsWith(tail.substring(0, tail.length() - 3));
    else
      return (name.equals(tail));
  }

  // private String mergePath(String path1, String path2) {
  // // path1 is xpath path
  // // path2 is dotted path
  // String[] parts = path2.split("\\.");
  // StringBuilder b = new StringBuilder(path1);
  // for (int i = 1; i < parts.length -1; i++)
  // b.append("/f:"+parts[i]);
  // return b.toString();
  // }

  private boolean passesCodeWhitespaceRules(String v) {
    if (!v.trim().equals(v))
      return false;
    boolean lastWasSpace = true;
    for (char c : v.toCharArray()) {
      if (c == ' ') {
        if (lastWasSpace)
          return false;
        else
          lastWasSpace = true;
      } else if (Character.isWhitespace(c))
        return false;
      else
        lastWasSpace = false;
    }
    return true;
  }

  private Element localResolve(String ref, NodeStack stack, List<ValidationMessage> errors, String path) {
    if (ref.startsWith("#")) {
      // work back through the contained list.
      // really, there should only be one level for this (contained resources cannot contain
      // contained resources), but we'll leave that to some other code to worry about
      while (stack != null && stack.getElement() != null) {
        if (stack.getElement().getProperty().isResource()) {
          // ok, we'll try to find the contained reference
          Element res = getContainedById(stack.getElement(), ref.substring(1));
          if (res != null)
            return res;
        }
        if (stack.getElement().getSpecial() == SpecialElement.BUNDLE_ENTRY) {
          return null; // we don't try to resolve contained references across this boundary
        }
        stack = stack.parent;
      }
      return null;
    } else {
      // work back through the contained list - if any of them are bundles, try to resolve
      // the resource in the bundle
      String fullUrl = null; // we're going to try to work this out as we go up
      while (stack != null && stack.getElement() != null) {
        if (stack.getElement().getSpecial() == SpecialElement.BUNDLE_ENTRY && fullUrl==null && stack.parent.getElement().getName().equals("entry")) {
          fullUrl = stack.parent.getElement().getChildValue("fullUrl"); // we don't try to resolve contained references across this boundary
          if (fullUrl==null)
            rule(errors, IssueType.REQUIRED, stack.parent.getElement().line(), stack.parent.getElement().col(), stack.parent.getLiteralPath(), fullUrl!=null, "Bundle entry missing fullUrl");
        }
        if ("Bundle".equals(stack.getElement().getType())) {
          Element res = getFromBundle(stack.getElement(), ref, fullUrl, errors, path);
          return res;
        }
        stack = stack.parent;
      }
    }
    return null;
  }

  private Element resolve(Object appContext, String ref, NodeStack stack, List<ValidationMessage> errors, String path) throws IOException, FHIRException {
    Element local = localResolve(ref, stack, errors, path);
    if (local!=null)
      return local;
    if (fetcher == null)
      return null;
    return fetcher.fetch(appContext, ref);
  }

  private ValueSet resolveBindingReference(DomainResource ctxt, Type reference, String uri) {
    if (reference instanceof UriType) {
      long t = System.nanoTime();
      ValueSet fr = context.fetchResource(ValueSet.class, ((UriType) reference).getValue().toString());
      txTime = txTime + (System.nanoTime() - t);
      return fr;
    }
    else if (reference instanceof Reference) {
      String s = ((Reference) reference).getReference();
      if (s.startsWith("#")) {
        for (Resource c : ctxt.getContained()) {
          if (c.getId().equals(s.substring(1)) && (c instanceof ValueSet))
            return (ValueSet) c;
        }
        return null;
      } else {
        long t = System.nanoTime();
        String ref = ((Reference) reference).getReference();
        if (!Utilities.isAbsoluteUrl(ref))
          ref = resolve(uri, ref);
        ValueSet fr = context.fetchResource(ValueSet.class, ref);
        txTime = txTime + (System.nanoTime() - t);
        return fr;
      }
    }
    else
      return null;
  }

  private String resolve(String uri, String ref) {
	  if (isBlank(uri)) {
		  return ref;
	  }
	  String[] up = uri.split("\\/");
    String[] rp = ref.split("\\/");
    if (context.getResourceNames().contains(up[up.length-2]) && context.getResourceNames().contains(rp[0])) {
      StringBuilder b = new StringBuilder();
      for (int i = 0; i < up.length-2; i++) {
        b.append(up[i]);
        b.append("/");
      }
      b.append(ref);
      return b.toString();
    } else
      return ref;
  }

  private Element resolveInBundle(List<Element> entries, String ref, String fullUrl, String type, String id) {
    if (Utilities.isAbsoluteUrl(ref)) {
      // if the reference is absolute, then you resolve by fullUrl. No other thinking is required. 
      for (Element entry : entries) {
        String fu = entry.getNamedChildValue("fullUrl");
        if (ref.equals(fu))
          return entry;
      }
      return null;
    } else {
      // split into base, type, and id
      String u = null;
      if (fullUrl != null && fullUrl.endsWith(type+"/"+id))
        // fullUrl = complex
        u = fullUrl.substring((type+"/"+id).length())+ref;
      String[] parts = ref.split("\\/");
      if (parts.length >= 2) {
        String t = parts[0];
        String i = parts[1];
        for (Element entry : entries) {
          String fu = entry.getNamedChildValue("fullUrl");
          if (u != null && fullUrl.equals(u))
            return entry;
          if (u == null) {
            Element resource = entry.getNamedChild("resource");
            String et = resource.getType();
            String eid = resource.getNamedChildValue("id");
            if (t.equals(et) && i.equals(eid))
              return entry;
          }
        }
      }
      return null;
    }
  }

  private ElementDefinition resolveNameReference(StructureDefinitionSnapshotComponent snapshot, String contentReference) {
    for (ElementDefinition ed : snapshot.getElement())
      if (contentReference.equals("#"+ed.getId()))
        return ed;
    return null;
  }

  private StructureDefinition resolveProfile(StructureDefinition profile, String pr)  {
    if (pr.startsWith("#")) {
      for (Resource r : profile.getContained()) {
        if (r.getId().equals(pr.substring(1)) && r instanceof StructureDefinition)
          return (StructureDefinition) r;
      }
      return null;
    } else {
      long t = System.nanoTime();
      StructureDefinition fr = context.fetchResource(StructureDefinition.class, pr);
      sdTime = sdTime + (System.nanoTime() - t);
      return fr;
    }
  }

  private ElementDefinition resolveType(String type)  {
    if (logical != null)
      for (BundleEntryComponent be : logical.getEntry()) {
        if (be.hasResource() && be.getResource() instanceof StructureDefinition) {
          StructureDefinition sd = (StructureDefinition) be.getResource();
          if (sd.getId().equals(type))
            return sd.getSnapshot().getElement().get(0);
        }
      }
    String url = "http://hl7.org/fhir/StructureDefinition/" + type;
    long t = System.nanoTime();
    StructureDefinition sd = context.fetchResource(StructureDefinition.class, url);
    sdTime = sdTime + (System.nanoTime() - t);
    if (sd == null || !sd.hasSnapshot())
      return null;
    else
      return sd.getSnapshot().getElement().get(0);
  }

  public void setAnyExtensionsAllowed(boolean anyExtensionsAllowed) {
    this.anyExtensionsAllowed = anyExtensionsAllowed;
  }

  public IResourceValidator setBestPracticeWarningLevel(BestPracticeWarningLevel value) {
    bpWarnings = value;
    return this;
  }

  @Override
  public void setCheckDisplay(CheckDisplayOption checkDisplay) {
    this.checkDisplay = checkDisplay;
  }

  public void setSuppressLoincSnomedMessages(boolean suppressLoincSnomedMessages) {
    this.suppressLoincSnomedMessages = suppressLoincSnomedMessages;
  }

  public IdStatus getResourceIdRule() {
    return resourceIdRule;
  }

  public void setResourceIdRule(IdStatus resourceIdRule) {
    this.resourceIdRule = resourceIdRule;
  }


  public boolean isAllowXsiLocation() {
    return allowXsiLocation;
  }

  public void setAllowXsiLocation(boolean allowXsiLocation) {
    this.allowXsiLocation = allowXsiLocation;
  }

  /**
   * 
   * @param element
   *          - the candidate that might be in the slice
   * @param path
   *          - for reporting any errors. the XPath for the element
   * @param slice
   *          - the definition of how slicing is determined
   * @param ed
   *          - the slice for which to test membership
   * @param errors 
   * @param stack 
   * @return
   * @throws DefinitionException 
   * @throws DefinitionException 
   * @throws IOException 
   * @throws FHIRException
   */
  private boolean sliceMatches(ValidatorHostContext hostContext, Element element, String path, ElementDefinition slicer, ElementDefinition ed, StructureDefinition profile, List<ValidationMessage> errors, NodeStack stack) throws DefinitionException, FHIRException, IOException {
    if (!slicer.getSlicing().hasDiscriminator())
      return false; // cannot validate in this case

    ExpressionNode n = (ExpressionNode) ed.getUserData("slice.expression.cache");
    if (n == null) {
      long t = System.nanoTime();
      // GG: this approach is flawed because it treats discriminators individually rather than collectively 
      StringBuilder expression = new StringBuilder("true");
      for (ElementDefinitionSlicingDiscriminatorComponent s : slicer.getSlicing().getDiscriminator()) {
        String discriminator = s.getPath();
        if (s.getType() == DiscriminatorType.PROFILE)
          throw new FHIRException("Validating against slices with discriminators based on profiles is not yet supported by the FHIRPath engine: " + discriminator);
        // Todo: Fix this once FHIRPath (and this engine) supports a boolean function that test profile conformance
        
        ElementDefinition criteriaElement = getCriteriaForDiscriminator(path, ed, discriminator, profile);
        if (s.getType() == DiscriminatorType.TYPE) {
          String type = null;
          if (!criteriaElement.getPath().contains("[") && discriminator.contains("[")) {
            discriminator = discriminator.substring(0, discriminator.indexOf('['));
            String lastNode = tail(discriminator);
            type = tail(criteriaElement.getPath()).substring(lastNode.length());
            type = type.substring(0,1).toLowerCase() + type.substring(1);
          } else if (!criteriaElement.hasType() || criteriaElement.getType().size()==1) {
            if (discriminator.contains("["))
              discriminator = discriminator.substring(0, discriminator.indexOf('['));
            type = criteriaElement.getType().get(0).getCode();
          }
          if (type==null)
            throw new DefinitionException("Discriminator (" + discriminator + ") is based on type, but slice " + ed.getId() + " does not declare a type");
          if (discriminator.isEmpty())
            expression.append(" and this is " + type);
          else
            expression.append(" and " + discriminator + " is " + type);
        } else if (s.getType() == DiscriminatorType.EXISTS) {
          if (criteriaElement.hasMin() && criteriaElement.getMin()>=1)
            expression.append(" and (" + discriminator + ".exists())");
          else if (criteriaElement.hasMax() && criteriaElement.getMax().equals("0"))
            expression.append(" and (" + discriminator + ".exists().not())");
          else
            throw new FHIRException("Discriminator (" + discriminator + ") is based on element existence, but slice " + ed.getId() + " neither sets min>=1 or max=0");
        } else if (criteriaElement.hasFixed()) {
          buildFixedExpression(ed, expression, discriminator, criteriaElement);
        } else if (criteriaElement.hasPattern()) {
          buildPattternExpression(ed, expression, discriminator, criteriaElement);
        } else if (criteriaElement.hasBinding() && criteriaElement.getBinding().hasStrength() && criteriaElement.getBinding().getStrength().equals(BindingStrength.REQUIRED) && criteriaElement.getBinding().getValueSetReference()!=null) {
          expression.append(" and (" + discriminator + " memberOf '" + criteriaElement.getBinding().getValueSetReference().getReference() + "')");
        } else {
          throw new DefinitionException("Could not match discriminator (" + discriminator + ") for slice " + ed.getId() + " in profile " + profile.getUrl() + " - does not have fixed value, binding or existence assertions");
        }
      }

      try {
        n = fpe.parse(expression.toString());
      } catch (FHIRLexerException e) {
        throw new FHIRException("Problem processing expression "+expression +" in profile " + profile.getUrl() + " path " + path + ": " + e.getMessage());
      }
      fpeTime = fpeTime + (System.nanoTime() - t);
      ed.setUserData("slice.expression.cache", n);
    }

    String msg;
    boolean ok;
    try {
      long t = System.nanoTime();
      ok = fpe.evaluateToBoolean(hostContext, hostContext.resource, element, n);
      fpeTime = fpeTime + (System.nanoTime() - t);
      msg = fpe.forLog();
    } catch (Exception ex) {
      throw new FHIRException("Problem evaluating slicing expression for element in profile " + profile.getUrl() + " path " + path + " (fhirPath = "+n+"): " + ex.getMessage());
    }
    return ok;
  }

  private void buildPattternExpression(ElementDefinition ed, StringBuilder expression, String discriminator, ElementDefinition criteriaElement) throws DefinitionException {
    Type pattern = criteriaElement.getPattern();
    if (pattern instanceof CodeableConcept) {
      CodeableConcept cc = (CodeableConcept) pattern;
      buildCodeableConceptExpression(ed, expression, discriminator, cc);
    } else
      throw new DefinitionException("Unsupported fixed pattern type for discriminator(" + discriminator + ") for slice " + ed.getId() + ": " + pattern.getClass().getName());
  }

  private void buildCodeableConceptExpression(ElementDefinition ed, StringBuilder expression, String discriminator, CodeableConcept cc)
      throws DefinitionException {
    if (cc.hasText())
      throw new DefinitionException("Unsupported CodeableConcept pattern - using text - for discriminator(" + discriminator + ") for slice " + ed.getId());
    if (!cc.hasCoding() || cc.getCoding().size() > 1)
      throw new DefinitionException("Unsupported CodeableConcept pattern - must be just one coding - for discriminator(" + discriminator + ") for slice " + ed.getId());
    Coding c = cc.getCodingFirstRep();
    if (c.hasExtension() || cc.hasExtension())
      throw new DefinitionException("Unsupported CodeableConcept pattern - extensions are not allowed - for discriminator(" + discriminator + ") for slice " + ed.getId());
    expression.append(" and " + discriminator + ".coding.where(");
    boolean first = true;
    if (c.hasSystem()) {
      first = false;
      expression.append("system = '"+c.getSystem()+"'");
    }
    if (c.hasVersion()) {
      if (first) first = false; else expression.append(" and ");
      expression.append("version = '"+c.getVersion()+"'");
    }
    if (c.hasCode()) {
      if (first) first = false; else expression.append(" and ");
      expression.append("code = '"+c.getCode()+"'");
    }
    if (c.hasDisplay()) {
      if (first) first = false; else expression.append(" and ");
      expression.append("display = '"+c.getDisplay()+"'");
    }
    expression.append(").exists()");
  }

  private void buildFixedExpression(ElementDefinition ed, StringBuilder expression, String discriminator, ElementDefinition criteriaElement) throws DefinitionException {
    Type fixed = criteriaElement.getFixed();
    if (fixed instanceof CodeableConcept) {
      CodeableConcept cc = (CodeableConcept) fixed;
      buildCodeableConceptExpression(ed, expression, discriminator, cc);
    } else {
      expression.append(" and (" + discriminator + " = ");
      if (fixed instanceof StringType) {
        Gson gson = new Gson();
        String json = gson.toJson((StringType)fixed);
        String escapedString = json.substring(json.indexOf(":")+2);
        escapedString = escapedString.substring(0, escapedString.indexOf(",\"myStringValue")-1);
        expression.append("'" + escapedString + "'");
      } else if (fixed instanceof UriType) {
        expression.append("'" + ((UriType)fixed).asStringValue() + "'");
      } else if (fixed instanceof IntegerType) {
        expression.append(((IntegerType)fixed).asStringValue());
      } else if (fixed instanceof DecimalType) {
        expression.append(((IntegerType)fixed).asStringValue());
      } else if (fixed instanceof BooleanType) {
        expression.append(((BooleanType)fixed).asStringValue());
      } else  
        throw new DefinitionException("Unsupported fixed value type for discriminator(" + discriminator + ") for slice " + ed.getId() + ": " + fixed.getClass().getName());
      expression.append(")");
    }
  }

  // we assume that the following things are true:
  // the instance at root is valid against the schema and schematron
  // the instance validator had no issues against the base resource profile
  private void start(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element resource, Element element, StructureDefinition defn, NodeStack stack) throws FHIRException, FHIRException, IOException {
    // profile is valid, and matches the resource name
    ResourceProfiles resourceProfiles = getResourceProfiles(element, stack);
    if (!resourceProfiles.isProcessed())
      checkDeclaredProfiles(resourceProfiles, errors, resource, element, stack);
    
    if (!resourceProfiles.isProcessed()) {
      resourceProfiles.setProcessed();
      if (!resourceProfiles.hasProfiles() &&
          (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), defn.hasSnapshot(),
              "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided"))) {
        // Don't need to validate against the resource if there's a profile because the profile snapshot will include the relevant parts of the resources
        validateElement(hostContext, errors, defn, defn.getSnapshot().getElement().get(0), null, null, resource, element, element.getName(), stack, false);
      } 

      // specific known special validations
      if (element.getType().equals("Bundle"))
        validateBundle(errors, element, stack);
      if (element.getType().equals("Observation"))
        validateObservation(errors, element, stack);
      if (element.getType().equals("QuestionnaireResponse"))
        validateQuestionannaireResponse(errors, element, stack);
    }
    for (ProfileUsage profileUsage : resourceProfiles.uncheckedProfiles()) {
      profileUsage.setChecked();
// todo: re-enable this when I can deal with the impact...   (GG)   
//      if (!profileUsage.getProfile().getType().equals(resource.fhirType()))
//        throw new FHIRException("Profile type mismatch - resource is "+resource.fhirType()+", and profile is for "+profileUsage.getProfile().getType());
      validateElement(hostContext, errors, profileUsage.getProfile(), profileUsage.getProfile().getSnapshot().getElement().get(0), null, null, resource, element, element.getName(), stack, false);
    }
  }

  private void validateQuestionannaireResponse(List<ValidationMessage> errors, Element element, NodeStack stack) {
    Element q = element.getNamedChild("questionnaire");
    if (hint(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), q != null && isNotBlank(q.getNamedChildValue("reference")), "No questionnaire is identified, so no validation can be performed against the base questionnaire")) {
      long t = System.nanoTime();
      Questionnaire qsrc = context.fetchResource(Questionnaire.class, q.getNamedChildValue("reference"));
      sdTime = sdTime + (System.nanoTime() - t);
      if (warning(errors, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null, "The questionnaire could not be resolved, so no validation can be performed against the base questionnaire")) {
        boolean inProgress = "in-progress".equals(element.getNamedChildValue("status"));
        validateQuestionannaireResponseItems(qsrc, qsrc.getItem(), errors, element, stack, inProgress);        
      }
    }
  }

  private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress) {
    String text = element.getNamedChildValue("text");
    rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), Utilities.noString(text) || text.equals(qItem.getText()), "If text exists, it must match the questionnaire definition for linkId "+qItem.getLinkId());

    List<Element> answers = new ArrayList<Element>();
    element.getNamedChildren("answer", answers);
    if (inProgress)
      warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), (answers.size() > 0) || !qItem.getRequired(), "No response answer found for required item "+qItem.getLinkId());
    else
      rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), (answers.size() > 0) || !qItem.getRequired(), "No response answer found for required item "+qItem.getLinkId());
    if (answers.size() > 1)
      rule(errors, IssueType.INVALID, answers.get(1).line(), answers.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response answer item with this linkId allowed");

    for (Element answer : answers) {
      NodeStack ns = stack.push(answer, -1, null, null);
      switch (qItem.getType()) {
      case GROUP: 
        rule(errors, IssueType.STRUCTURE, answer.line(), answer.col(), stack.getLiteralPath(), false, "Items of type group should not have answers"); 
        break;
      case DISPLAY:  // nothing
        break;
      case BOOLEAN:       
        validateQuestionnaireResponseItemType(errors, answer, ns, "boolean");
        break;
      case DECIMAL:       
        validateQuestionnaireResponseItemType(errors, answer, ns, "decimal");
        break;
      case INTEGER:       
        validateQuestionnaireResponseItemType(errors, answer, ns, "integer");
        break;
      case DATE:          
        validateQuestionnaireResponseItemType(errors, answer, ns, "date");
        break;
      case DATETIME:      
        validateQuestionnaireResponseItemType(errors, answer, ns, "dateTime");
        break;
      case TIME:          
        validateQuestionnaireResponseItemType(errors, answer, ns, "time");
        break;
      case STRING:        
        validateQuestionnaireResponseItemType(errors, answer, ns, "string");
        break;
      case TEXT:          
        validateQuestionnaireResponseItemType(errors, answer, ns, "text");
        break;
      case URL:           
        validateQuestionnaireResponseItemType(errors, answer, ns, "uri");
        break;
      case ATTACHMENT:    
        validateQuestionnaireResponseItemType(errors, answer, ns, "Attachment");
        break;
      case REFERENCE:     
        validateQuestionnaireResponseItemType(errors, answer, ns, "Reference");
        break;
      case QUANTITY:   
        if (validateQuestionnaireResponseItemType(errors, answer, ns, "Quantity").equals("Quantity"))
          if (qItem.hasExtension("???"))
            validateQuestionnaireResponseItemQuantity(errors, answer, ns);
        break;
      case CHOICE:     
        String itemType=validateQuestionnaireResponseItemType(errors, answer, ns, "Coding", "date", "time", "integer", "string");
        if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, false);
        else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date");
        else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time");
        else if (itemType.equals("integer")) checkOption(errors, answer, ns, qsrc, qItem, "integer");
        else if (itemType.equals("string")) checkOption(errors, answer, ns, qsrc, qItem, "string");
        break;
      case OPENCHOICE: 
        itemType=validateQuestionnaireResponseItemType(errors, answer, ns, "Coding", "date", "time", "integer", "string");
        if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, true);
        else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date");
        else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time");
        else if (itemType.equals("integer")) checkOption(errors, answer, ns, qsrc, qItem, "integer");
        else if (itemType.equals("string")) checkOption(errors, answer, ns, qsrc, qItem, "string", true);
        break;
      case QUESTION:
        // TODO: how to validate this???
        break;
      case NULL:
        // Should not happen
        break;
      }
      validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress);
    }
    if (qItem.getType() == null) {
      fail(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, "Definition for item "+qItem.getLinkId() + " does not contain a type");
    } else if (qItem.getType() == QuestionnaireItemType.GROUP) {
      validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, element, stack, inProgress);
    } else {
      List<Element> items = new ArrayList<Element>();
      element.getNamedChildren("item", items);
      for (Element item : items) {
        NodeStack ns = stack.push(item, -1, null, null);
        rule(errors, IssueType.STRUCTURE, answers.get(0).line(), answers.get(0).col(), stack.getLiteralPath(), false, "Items not of type group should not have items - Item with linkId {0} of type {1} has {2} item(s)", qItem.getLinkId(), qItem.getType(), items.size());
      }
    }
  }

  private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, List<Element> elements, NodeStack stack, boolean inProgress) {
    if (elements.size() > 1)
      rule(errors, IssueType.INVALID, elements.get(1).line(), elements.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response item with this linkId allowed");
    for (Element element : elements) {
      NodeStack ns = stack.push(element, -1, null, null);
      validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress);
    }
  }

  private int getLinkIdIndex(List<QuestionnaireItemComponent> qItems, String linkId) {
    for (int i = 0; i < qItems.size(); i++) {
      if (linkId.equals(qItems.get(i).getLinkId()))
        return i;
    }
    return -1;
  }

  private void validateQuestionannaireResponseItems(Questionnaire qsrc, List<QuestionnaireItemComponent> qItems, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress) {
    List<Element> items = new ArrayList<Element>();
    element.getNamedChildren("item", items);
    // now, sort into stacks
    Map<String, List<Element>> map = new HashMap<String, List<Element>>();
    int lastIndex = -1;
    for (Element item : items) {
      String linkId = item.getNamedChildValue("linkId");
      if (rule(errors, IssueType.REQUIRED, item.line(), item.col(), stack.getLiteralPath(), !Utilities.noString(linkId), "No LinkId, so can't be validated")) {
        int index = getLinkIdIndex(qItems, linkId);
        if (index == -1) {
          QuestionnaireItemComponent qItem = findQuestionnaireItem(qsrc, linkId);
          if (qItem != null) {
            rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, "Structural Error: item is in the wrong place");
            NodeStack ns = stack.push(item, -1, null, null);
            validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress);
          }
          else
            rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, "LinkId \""+linkId+"\" not found in questionnaire");
        }
        else
        {
          rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index >= lastIndex, "Structural Error: items are out of order");
          lastIndex = index;
          List<Element> mapItem = map.get(linkId);
          if (mapItem == null) {
            mapItem = new ArrayList<Element>();
            map.put(linkId, mapItem);
          }
          mapItem.add(item);
        }
      }
    }

    // ok, now we have a list of known items, grouped by linkId. We"ve made an error for anything out of order
    for (QuestionnaireItemComponent qItem : qItems) {
      List<Element> mapItem = map.get(qItem.getLinkId());
      if (mapItem != null)
        validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress);
      else
        rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), !qItem.getRequired(), "No response found for required item "+qItem.getLinkId());
    }
  }

  private void validateQuestionnaireResponseItemQuantity( List<ValidationMessage> errors, Element answer, NodeStack stack)	{

  }

  private String validateQuestionnaireResponseItemType(List<ValidationMessage> errors, Element element, NodeStack stack, String... types) {
    List<Element> values = new ArrayList<Element>();
    element.getNamedChildrenWithWildcard("value[x]", values);
    if (values.size() > 0) {
      NodeStack ns = stack.push(values.get(0), -1, null, null);
      CommaSeparatedStringBuilder l = new CommaSeparatedStringBuilder();
      for (String s : types)  {
        l.append(s);
        if (values.get(0).getName().equals("value"+Utilities.capitalize(s))) 
          return(s);
      }
      if (types.length == 1)
        rule(errors, IssueType.STRUCTURE, values.get(0).line(), values.get(0).col(), ns.getLiteralPath(), false, "Answer value must be of type "+types[0]);
      else
        rule(errors, IssueType.STRUCTURE, values.get(0).line(), values.get(0).col(), ns.getLiteralPath(), false, "Answer value must be one of the types "+l.toString());
    }
    return null;
  }

  private QuestionnaireItemComponent findQuestionnaireItem(Questionnaire qSrc, String linkId) {
    return findItem(qSrc.getItem(), linkId);
  }

  private QuestionnaireItemComponent findItem(List<QuestionnaireItemComponent> list, String linkId) {
    for (QuestionnaireItemComponent item : list) {
      if (linkId.equals(item.getLinkId()))
        return item;
      QuestionnaireItemComponent result = findItem(item.getItem(), linkId);
      if (result != null)
        return result;
    }
    return null;
  }

  /*	private void validateAnswerCode(List<ValidationMessage> errors, Element value, NodeStack stack, List<Coding> optionList) {
	  String system = value.getNamedChildValue("system");
	  String code = value.getNamedChildValue("code");
	  boolean found = false;
	  for (Coding c : optionList) {
      if (ObjectUtil.equals(c.getSystem(), system) && ObjectUtil.equals(c.getCode(), code)) {
	      found = true;
	      break;
	    }
	  }
	  rule(errors, IssueType.STRUCTURE, value.line(), value.col(), stack.getLiteralPath(), found, "The code "+system+"::"+code+" is not a valid option");
	}*/

  private void validateAnswerCode(List<ValidationMessage> errors, Element value, NodeStack stack, Questionnaire qSrc, Reference ref, boolean theOpenChoice) {
    ValueSet vs = resolveBindingReference(qSrc, ref, qSrc.getUrl());
    if (warning(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), vs != null, "ValueSet " + describeReference(ref) + " not found"))  {
      try {
        Coding c = ObjectConverter.readAsCoding(value);
        if (isBlank(c.getCode()) && isBlank(c.getSystem()) && isNotBlank(c.getDisplay())) {
          if (theOpenChoice) {
            return;
          }
        }

        long t = System.nanoTime();
        ValidationResult res = context.validateCode(c, vs);
        txTime = txTime + (System.nanoTime() - t);
        if (!res.isOk())
          rule(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, "The value provided ("+c.getSystem()+"::"+c.getCode()+") is not in the options value set in the questionnaire");
      } catch (Exception e) {
        warning(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, "Error " + e.getMessage() + " validating Coding against Questionnaire Options");
      }
    }
  }

  private void validateAnswerCode( List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean theOpenChoice) {
    Element v = answer.getNamedChild("valueCoding");
    NodeStack ns = stack.push(v, -1, null, null);
    if (qItem.getOption().size() > 0)
      checkCodingOption(errors, answer, stack, qSrc, qItem, theOpenChoice);
    //	    validateAnswerCode(errors, v, stack, qItem.getOption());
    else if (qItem.hasOptions())
      validateAnswerCode(errors, v, stack, qSrc, qItem.getOptions(), theOpenChoice);
    else
      hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate options because no option or options are provided");
  }

  private void checkOption( List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, String type) {
    checkOption(errors, answer, stack, qSrc,  qItem, type, false);
  }

  private void checkOption( List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, String type, boolean openChoice) {
    if (type.equals("integer"))     checkIntegerOption(errors, answer, stack, qSrc, qItem, openChoice);
    else if (type.equals("date"))   checkDateOption(errors, answer, stack, qSrc, qItem, openChoice);
    else if (type.equals("time"))   checkTimeOption(errors, answer, stack, qSrc, qItem, openChoice);
    else if (type.equals("string")) checkStringOption(errors, answer, stack, qSrc, qItem, openChoice);
    else if (type.equals("Coding")) checkCodingOption(errors, answer, stack, qSrc, qItem, openChoice);
  }

  private void checkIntegerOption( List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
    Element v = answer.getNamedChild("valueInteger");
    NodeStack ns = stack.push(v, -1, null, null);
    if (qItem.getOption().size() > 0) {
      List<IntegerType> list = new ArrayList<IntegerType>();
      for (QuestionnaireItemOptionComponent components : qItem.getOption())  {
        try {
          list.add(components.getValueIntegerType());
        } catch (FHIRException e) {
          // If it's the wrong type, just keep going
        }
      }
      if (list.isEmpty() && !openChoice) {
        rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type integer");
      } else {
        boolean found = false;
        for (IntegerType item : list) {
          if (item.getValue() == Integer.parseInt(v.primitiveValue())) {
            found = true;
            break;
          }
        }
        if (!found) {
          rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The integer "+v.primitiveValue()+" is not a valid option");
        }
      }
    } else
      hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate integer answer option because no option list is provided");
  }

  private void checkDateOption( List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
    Element v = answer.getNamedChild("valueDate");
    NodeStack ns = stack.push(v, -1, null, null);
    if (qItem.getOption().size() > 0) {
      List<DateType> list = new ArrayList<DateType>();
      for (QuestionnaireItemOptionComponent components : qItem.getOption())  {
        try {
          list.add(components.getValueDateType());
        } catch (FHIRException e) {
          // If it's the wrong type, just keep going
        }
      }
      if (list.isEmpty() && !openChoice) {
        rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type date");
      } else {
        boolean found = false;
        for (DateType item : list) {
          if (item.getValue().equals(v.primitiveValue())) {
            found = true;
            break;
          }
        }
        if (!found) {
          rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The date "+v.primitiveValue()+" is not a valid option");
        }
      }
    } else
      hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate date answer option because no option list is provided");
  }

  private void checkTimeOption( List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
    Element v = answer.getNamedChild("valueTime");
    NodeStack ns = stack.push(v, -1, null, null);
    if (qItem.getOption().size() > 0) {
      List<TimeType> list = new ArrayList<TimeType>();
      for (QuestionnaireItemOptionComponent components : qItem.getOption())  {
        try {
          list.add(components.getValueTimeType());
        } catch (FHIRException e) {
          // If it's the wrong type, just keep going
        }
      }
      if (list.isEmpty() && !openChoice) {
        rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type time");
      } else {
        boolean found = false;
        for (TimeType item : list) {
          if (item.getValue().equals(v.primitiveValue())) {
            found = true;
            break;
          }
        }
        if (!found) {
          rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The time "+v.primitiveValue()+" is not a valid option");
        }
      }
    } else
      hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate time answer option because no option list is provided");
  }

  private void checkStringOption( List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
    Element v = answer.getNamedChild("valueString");
    NodeStack ns = stack.push(v, -1, null, null);
    if (qItem.getOption().size() > 0) {
      List<StringType> list = new ArrayList<StringType>();
      for (QuestionnaireItemOptionComponent components : qItem.getOption())  {
        try {
          if (components.getValue() != null) {
            list.add(components.getValueStringType());
          }
        } catch (FHIRException e) {
          // If it's the wrong type, just keep going
        }
      }
      if (list.isEmpty() && !openChoice) {
        rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type string");
      } else {
        boolean found = false;
        for (StringType item : list) {
          if (item.getValue().equals((v.primitiveValue()))) {
            found = true;
            break;
          }
        }
        if (!found) {
          rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The string "+v.primitiveValue()+" is not a valid option");
        }
      }
    } else {
      hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate string answer option because no option list is provided");
    }
  }

  private void checkCodingOption( List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
    Element v = answer.getNamedChild("valueCoding");
    String system = v.getNamedChildValue("system");
    String code = v.getNamedChildValue("code");
    NodeStack ns = stack.push(v, -1, null, null);
    if (qItem.getOption().size() > 0) {
      List<Coding> list = new ArrayList<Coding>();
      for (QuestionnaireItemOptionComponent components : qItem.getOption())  {
        try {
          if (components.getValue() != null) {
            list.add(components.getValueCoding());
          }
        } catch (FHIRException e) {
          // If it's the wrong type, just keep going
        }
      }
      if (list.isEmpty() && !openChoice) {
        rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Option list has no option values of type coding");
      } else {
        boolean found = false;
        for (Coding item : list) {
          if (ObjectUtil.equals(item.getSystem(), system) && ObjectUtil.equals(item.getCode(), code)) {
            found = true;
            break;
          }
        }
        if (!found) {
          rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, "The code "+system+"::"+code+" is not a valid option");
        }
      }
    } else
      hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate Coding option because no option list is provided");
  }

  private String tail(String path) {
    return path.substring(path.lastIndexOf(".") + 1);
  }

  private String tryParse(String ref)  {
    String[] parts = ref.split("\\/");
    switch (parts.length) {
    case 1:
      return null;
    case 2:
      return checkResourceType(parts[0]);
    default:
      if (parts[parts.length - 2].equals("_history"))
        return checkResourceType(parts[parts.length - 4]);
      else
        return checkResourceType(parts[parts.length - 2]);
    }
  }

  private boolean typesAreAllReference(List<TypeRefComponent> theType) {
    for (TypeRefComponent typeRefComponent : theType) {
      if (typeRefComponent.getCode().equals("Reference") == false) {
        return false;
      }
    }
    return true;
  }

  private void validateBundle(List<ValidationMessage> errors, Element bundle, NodeStack stack) {
    List<Element> entries = new ArrayList<Element>();
    bundle.getNamedChildren("entry", entries);
    String type = bundle.getNamedChildValue("type");
    type = StringUtils.defaultString(type);

    if (entries.size() == 0) {
      rule(errors, IssueType.INVALID, stack.getLiteralPath(), !(type.equals("document") || type.equals("message")), "Documents or Messages must contain at least one entry");
    } else {
      Element firstEntry = entries.get(0);
      NodeStack firstStack = stack.push(firstEntry, 0, null, null);
      String fullUrl = firstEntry.getNamedChildValue("fullUrl");

      if (type.equals("document")) {
        Element resource = firstEntry.getNamedChild("resource");
        NodeStack localStack = firstStack.push(resource, -1, null, null);
        String id = resource.getNamedChildValue("id");
        if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath("entry", ":0"), resource != null, "No resource on first entry")) {
          validateDocument(errors, entries, resource, localStack.push(resource, -1, null, null), fullUrl, id);
        }
        checkAllInterlinked(errors, entries, stack, bundle);
      }
      if (type.equals("message")) {
        Element resource = firstEntry.getNamedChild("resource");
        NodeStack localStack = firstStack.push(resource, -1, null, null);
        String id = resource.getNamedChildValue("id");
        if (rule(errors, IssueType.INVALID, firstEntry.line(), firstEntry.col(), stack.addToLiteralPath("entry", ":0"), resource != null, "No resource on first entry")) {
          validateMessage(errors, entries, resource, localStack.push(resource, -1, null, null), fullUrl, id);
        }
        checkAllInterlinked(errors, entries, stack, bundle);
      }
    }
  }

  private void checkAllInterlinked(List<ValidationMessage> errors, List<Element> entries, NodeStack stack, Element bundle) {
    List<Element> visitedResources = new ArrayList<Element>();
    HashMap<Element,Element> candidateEntries = new HashMap<Element,Element>();
    List<Element> candidateResources = new ArrayList<Element>();
    for (Element entry: entries) {
      candidateEntries.put(entry.getNamedChild("resource"), entry);
      candidateResources.add(entry.getNamedChild("resource"));
    }
    followResourceLinks(entries.get(0), visitedResources, candidateEntries, candidateResources, true, errors, stack);
    List<Element> unusedResources = new ArrayList<Element>();
    unusedResources.addAll(candidateResources);
    unusedResources.removeAll(visitedResources);
    int i = 0;
    for (Element entry : entries) {
      // TODO: Need to add support for tracking Bundle.link entries so that embedded stylesheets won't trigger this
      warning(errors, IssueType.INFORMATIONAL, entry.line(), entry.col(), stack.addToLiteralPath("entry", Integer.toString(i)), !unusedResources.contains(entry.getNamedChild("resource")), "Entry isn't reachable by traversing from first Bundle entry");
      i++;
    }
    // Todo - check if the remaining resources point *to* the elements in the referenced set.  Any that are still left over are errors
  }

  private void followResourceLinks(Element entry, List<Element> visitedResources, HashMap<Element, Element> candidateEntries, List<Element> candidateResources, boolean referenced, List<ValidationMessage> errors, NodeStack stack) {
    Element resource = entry.getNamedChild("resource");
    if (visitedResources.contains(resource))
      return;

    if (referenced)
      visitedResources.add(resource);

    List<String> references = findReferences(resource);
    for (String reference: references) {
      // We don't want errors when just retrieving the element as they will be caught (with better path info) in subsequent processing
      Element r = getFromBundle(stack.getElement(), reference, entry.getChildValue("fullUrl"), new ArrayList<ValidationMessage>(), stack.addToLiteralPath("entry[" + candidateResources.indexOf(resource) + "]"));
      if (r!=null && !visitedResources.contains(r)) {
        followResourceLinks(candidateEntries.get(r), visitedResources, candidateEntries, candidateResources, referenced, errors, stack);
      }
    }
  }

  private List<String> findReferences(Element start) {
    List<String> references = new ArrayList<String>();
    findReferences(start, references);
    return references;
  }

  private void findReferences(Element start, List<String> references) {
    for (Element child : start.getChildren()) {
      if (child.getType().equals("Reference")) {
        String ref = child.getChildValue("reference");
        if (ref!=null && !ref.startsWith("#"))
          references.add(ref);
      }
      findReferences(child, references);
    }
  }

  private void validateBundleReference(List<ValidationMessage> errors, List<Element> entries, Element ref, String name, NodeStack stack, String fullUrl, String type, String id) {
    String reference = null;
    try {
      reference = ref.getNamedChildValue("reference");
    } catch (Error e) {
      
    }
        
    if (ref != null && !Utilities.noString(reference)) {
      Element target = resolveInBundle(entries, reference, fullUrl, type, id);
      rule(errors, IssueType.INVALID, ref.line(), ref.col(), stack.addToLiteralPath("reference"), target != null, "Unable to resolve the target of the reference in the bundle (" + name + ")");
    }
  }

  private void validateContains(ValidatorHostContext hostContext, List<ValidationMessage> errors, String path, ElementDefinition child, ElementDefinition context, Element resource, Element element, NodeStack stack, IdStatus idstatus) throws FHIRException, FHIRException, IOException {
    String resourceName = element.getType();
    long t = System.nanoTime();
    StructureDefinition profile = this.context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName);
    sdTime = sdTime + (System.nanoTime() - t);
    // special case: resource wrapper is reset if we're crossing a bundle boundary, but not otherwise
    if (element.getSpecial() == SpecialElement.BUNDLE_ENTRY || element.getSpecial() == SpecialElement.BUNDLE_OUTCOME || element.getSpecial() == SpecialElement.PARAMETER ) 
      resource = element;
    if (rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), profile != null, "No profile found for contained resource of type '" + resourceName + "'"))
      validateResource(hostContext.forContained(element), errors, resource, element, profile, null, idstatus, stack, false);
  }

  private void validateDocument(List<ValidationMessage> errors, List<Element> entries, Element composition, NodeStack stack, String fullUrl, String id) {
    // first entry must be a composition
    if (rule(errors, IssueType.INVALID, composition.line(), composition.col(), stack.getLiteralPath(), composition.getType().equals("Composition"),
        "The first entry in a document must be a composition")) {
      // the composition subject and section references must resolve in the bundle
      Element elem = composition.getNamedChild("subject");
      if (rule(errors, IssueType.INVALID, composition.line(), composition.col(), stack.getLiteralPath(), elem != null, "A document composition must have a subject"))
        validateBundleReference(errors, entries, elem, "Composition Subject", stack.push(elem, -1, null, null), fullUrl, "Composition", id);
      validateSections(errors, entries, composition, stack, fullUrl, id);
    }
  }
  // rule(errors, IssueType.INVALID, bundle.line(), bundle.col(), "Bundle", !"urn:guid:".equals(base), "The base 'urn:guid:' is not valid (use urn:uuid:)");
  // rule(errors, IssueType.INVALID, entry.line(), entry.col(), localStack.getLiteralPath(), !"urn:guid:".equals(ebase), "The base 'urn:guid:' is not valid");
  // rule(errors, IssueType.INVALID, entry.line(), entry.col(), localStack.getLiteralPath(), !Utilities.noString(base) || !Utilities.noString(ebase), "entry
  // does not have a base");
  // String firstBase = null;
  // firstBase = ebase == null ? base : ebase;

  private void validateElement(ValidatorHostContext hostContext, List<ValidationMessage> errors, StructureDefinition profile, ElementDefinition definition, StructureDefinition cprofile, ElementDefinition context,
      Element resource, Element element, String actualType, NodeStack stack, boolean inCodeableConcept) throws FHIRException, FHIRException, IOException {
    element.markValidation(profile, definition);

    //		System.out.println("  "+stack.getLiteralPath()+" "+Long.toString((System.nanoTime() - time) / 1000000));
    //		time = System.nanoTime();
    if (resource.getName().equals("contained")) {
      NodeStack ancestor = stack;
      while (!ancestor.element.isResource() || ancestor.element.getName().equals("contained"))
        ancestor = ancestor.parent;
      checkInvariants(hostContext, errors, stack.getLiteralPath(), profile, definition, null, null, ancestor.element, element);
    } else
      checkInvariants(hostContext, errors, stack.getLiteralPath(), profile, definition, null, null, resource, element);
    if (definition.getFixed()!=null)
      checkFixedValue(errors, stack.getLiteralPath(), element, definition.getFixed(), definition.getSliceName(), null);


    // get the list of direct defined children, including slices
    List<ElementDefinition> childDefinitions = ProfileUtilities.getChildMap(profile, definition);
    if (childDefinitions.isEmpty()) {
      if (actualType == null)
        return; // there'll be an error elsewhere in this case, and we're going to stop.

      StructureDefinition dt = this.context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + actualType);
      if (dt == null)
        throw new DefinitionException("Unable to resolve actual type " + actualType);

      childDefinitions = ProfileUtilities.getChildMap(dt, dt.getSnapshot().getElement().get(0));
    }

    // 1. List the children, and remember their exact path (convenience)
    List<ElementInfo> children = new ArrayList<InstanceValidator.ElementInfo>();
    ChildIterator iter = new ChildIterator(stack.getLiteralPath(), element);
    while (iter.next())
      children.add(new ElementInfo(iter.name(), iter.element(), iter.path(), iter.count()));

    // 2. assign children to a definition
    // for each definition, for each child, check whether it belongs in the slice
    ElementDefinition slicer = null;
    boolean unsupportedSlicing = false;
    List<String> problematicPaths = new ArrayList<String>();
    String slicingPath = null;
    int sliceOffset = 0;
    for (int i = 0; i < childDefinitions.size(); i++) {
      ElementDefinition ed = childDefinitions.get(i);
      boolean childUnsupportedSlicing = false;
      boolean process = true;
      if (ed.hasSlicing() && !ed.getSlicing().getOrdered())
        slicingPath = ed.getPath();
      else if (slicingPath!=null && ed.getPath().equals(slicingPath))
        ; // nothing
      else if (slicingPath != null && !ed.getPath().startsWith(slicingPath))
        slicingPath = null;
      // where are we with slicing
      if (ed.hasSlicing()) {
        if (slicer != null && slicer.getPath().equals(ed.getPath()))
          throw new DefinitionException("Slice encountered midway through path on " + slicer.getPath());
        slicer = ed;
        process = false;
        sliceOffset = i;
      } else if (slicer != null && !slicer.getPath().equals(ed.getPath()))
        slicer = null;

//      if (process) {
        for (ElementInfo ei : children) {
          boolean match = false;
          if (slicer == null || slicer == ed) {
            match = nameMatches(ei.name, tail(ed.getPath()));
          } else {
//            ei.slice = slice;
            if (nameMatches(ei.name, tail(ed.getPath())))
              try {
                match = sliceMatches(hostContext, ei.element, ei.path, slicer, ed, profile, errors, stack);
                if (match)
                  ei.slice = slicer;
              } catch (FHIRException e) {
                rule(errors, IssueType.PROCESSING, ei.line(), ei.col(), ei.path, false, e.getMessage());
                unsupportedSlicing = true;
                childUnsupportedSlicing = true;
              }
          }
          if (match) {
            if (rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, ei.definition == null || ei.definition == slicer, "Profile " + profile.getUrl() + ", Element matches more than one slice - " + (ei.definition==null || !ei.definition.hasSliceName() ? "" : ei.definition.getSliceName()) + ", " + (ed.hasSliceName() ? ed.getSliceName() : ""))) {
              ei.definition = ed;
              if (ei.slice == null) {
                ei.index = i;
              } else {
                ei.index = sliceOffset;
                ei.sliceindex = i - (sliceOffset + 1);
              }
            }
          } else if (childUnsupportedSlicing) {
            problematicPaths.add(ed.getPath());
          }
        }
//      }
    }
    int last = -1;
    int lastSlice = -1;
    for (ElementInfo ei : children) {
      String sliceInfo = "";
      if (slicer != null)
        sliceInfo = " (slice: " + slicer.getPath()+")";
      if (ei.path.endsWith(".extension"))
        rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, ei.definition != null, "Element is unknown or does not match any slice (url=\"" + ei.element.getNamedChildValue("url") + "\")" + (profile==null ? "" : " for profile " + profile.getUrl()));
      else if (!unsupportedSlicing)
        if (ei.slice!=null && (ei.slice.getSlicing().getRules().equals(ElementDefinition.SlicingRules.OPEN)))
          hint(errors, IssueType.INFORMATIONAL, ei.line(), ei.col(), ei.path, (ei.definition != null),
              "Element " + ei.element.getName() + " is unknown or does not match any slice " + sliceInfo + (profile==null ? "" : " for profile " + profile.getUrl()));
        else
          if (ei.slice!=null && (ei.slice.getSlicing().getRules().equals(ElementDefinition.SlicingRules.OPEN)))
            rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, (ei.definition != null),
                "Element " + ei.element.getName() + " is unknown or does not match any slice " + sliceInfo + (profile==null ? "" : " for profile " + profile.getUrl()));
          else
            hint(errors, IssueType.NOTSUPPORTED, ei.line(), ei.col(), ei.path, (ei.definition != null),
                "Could not verify slice for profile " + profile.getUrl());
      // TODO: Should get the order of elements correct when parsing elements that are XML attributes vs. elements
      boolean isXmlAttr = false;
      if (ei.definition!=null)
        for (Enumeration<PropertyRepresentation> r : ei.definition.getRepresentation()) {
          if (r.getValue() == PropertyRepresentation.XMLATTR) {
            isXmlAttr = true;
            break;
          }
        }

      rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, (ei.definition == null) || (ei.index >= last) || isXmlAttr, "As specified by profile " + profile.getUrl() + ", Element '"+ei.name+"' is out of order");
      if (ei.slice != null && ei.index == last && ei.slice.getSlicing().getOrdered())
        rule(errors, IssueType.INVALID, ei.line(), ei.col(), ei.path, (ei.definition == null) || (ei.sliceindex >= lastSlice) || isXmlAttr, "As specified by profile " + profile.getUrl() + ", Element  '"+ei.name+"' is out of order in ordered slice");
      if (ei.definition == null || !isXmlAttr)
        last = ei.index;
      if (ei.slice != null)
        lastSlice = ei.sliceindex;
      else
        lastSlice = -1;
    }

    // 3. report any definitions that have a cardinality problem
    for (ElementDefinition ed : childDefinitions) {
      if (ed.getRepresentation().isEmpty()) { // ignore xml attributes
        int count = 0;
        List<ElementDefinition> slices = null;
        if (ed.hasSlicing())
          slices = ProfileUtilities.getSliceList(profile, ed);
        for (ElementInfo ei : children)
          if (ei.definition == ed)
            count++;
          else if (slices!=null) {
            for (ElementDefinition sed : slices) {
              if (ei.definition == sed) {
                count++;
                break;
              }
            }
          }
        String location = "Profile " + profile.getUrl() + ", Element '" + stack.getLiteralPath() + "." + tail(ed.getPath()) + (ed.hasSliceName()? "[" + ed.getSliceName() + (ed.hasLabel() ? " ("+ed.getLabel()+")" : "")+"]": "");
        if (ed.getMin() > 0) {
          if (problematicPaths.contains(ed.getPath()))
            hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(),
            location + "': Unable to check minimum required (" + Integer.toString(ed.getMin()) + ") due to lack of slicing validation");
          else
            rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(),
            location + "': minimum required = " + Integer.toString(ed.getMin()) + ", but only found " + Integer.toString(count));
        }
        if (ed.hasMax() && !ed.getMax().equals("*")) {
          if (problematicPaths.contains(ed.getPath()))
            hint(errors, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()),
            location + ": Unable to check max allowed (" + ed.getMax() + ") due to lack of slicing validation");
          else
            rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()),
            location + ": max allowed = " + ed.getMax() + ", but found " + Integer.toString(count));
        }
      }
    }
    // 4. check order if any slices are ordered. (todo)

    // 5. inspect each child for validity
    for (ElementInfo ei : children) {
      List<String> profiles = new ArrayList<String>();
      if (ei.definition != null) {
        String type = null;
        ElementDefinition typeDefn = null;
        
        String usesMustSupport = profile.getUserString("usesMustSupport"); 
        if (usesMustSupport == null) {
          usesMustSupport = "N";
          for (ElementDefinition pe: profile.getSnapshot().getElement()) {
            if (pe.getMustSupport()) {
              usesMustSupport = "Y";
              break;
            }
          }
          profile.setUserData("usesMustSupport", usesMustSupport);
        }
        if (usesMustSupport.equals("Y")) {
          String elementSupported = ei.element.getUserString("elementSupported");
          if (elementSupported==null || ei.definition.getMustSupport())
            if (ei.definition.getMustSupport())
              ei.element.setUserData("elementSupported", "Y");
            else
              ei.element.setUserData("elementSupported", "N");
        }
        
        if (ei.definition.getType().size() == 1 && !ei.definition.getType().get(0).getCode().equals("*") && !ei.definition.getType().get(0).getCode().equals("Element")
            && !ei.definition.getType().get(0).getCode().equals("BackboneElement")) {
          type = ei.definition.getType().get(0).getCode();
          // Excluding reference is a kludge to get around versioning issues
          if (ei.definition.getType().get(0).hasProfile() && !type.equals("Reference"))
            profiles.add(ei.definition.getType().get(0).getProfile());
          
        } else if (ei.definition.getType().size() == 1 && ei.definition.getType().get(0).getCode().equals("*")) {
          String prefix = tail(ei.definition.getPath());
          assert prefix.endsWith("[x]");
          type = ei.name.substring(prefix.length() - 3);
          if (isPrimitiveType(type))
            type = Utilities.uncapitalize(type);
          // Excluding reference is a kludge to get around versioning issues
          if (ei.definition.getType().get(0).hasProfile() && !type.equals("Reference"))
            profiles.add(ei.definition.getType().get(0).getProfile());
        } else if (ei.definition.getType().size() > 1) {

          String prefix = tail(ei.definition.getPath());
          assert typesAreAllReference(ei.definition.getType()) || prefix.endsWith("[x]") : prefix;

          prefix = prefix.substring(0, prefix.length() - 3);
          for (TypeRefComponent t : ei.definition.getType())
            if ((prefix + Utilities.capitalize(t.getCode())).equals(ei.name)) {
              type = t.getCode();
              // Excluding reference is a kludge to get around versioning issues
              if (t.hasProfile() && !type.equals("Reference"))
                profiles.add(t.getProfile());
            }
          if (type == null) {
            TypeRefComponent trc = ei.definition.getType().get(0);
            if (trc.getCode().equals("Reference"))
              type = "Reference";
            else 
              rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), false,
                  "The element " + ei.name + " is illegal. Valid types at this point are " + describeTypes(ei.definition.getType()));
          }
        } else if (ei.definition.getContentReference() != null) {
          typeDefn = resolveNameReference(profile.getSnapshot(), ei.definition.getContentReference());
        }

        if (type != null) {
          if (type.startsWith("@")) {
            ei.definition = findElement(profile, type.substring(1));
            type = null;
          }
        }
        NodeStack localStack = stack.push(ei.element, ei.count, ei.definition, type == null ? typeDefn : resolveType(type));
        String localStackLiterapPath = localStack.getLiteralPath();
        String eiPath = ei.path;
        assert(eiPath.equals(localStackLiterapPath)) : "ei.path: " + ei.path + "  -  localStack.getLiteralPath: " + localStackLiterapPath;
        boolean thisIsCodeableConcept = false;

        if (type != null) {
          if (isPrimitiveType(type)) {
            checkPrimitive(hostContext, errors, ei.path, type, ei.definition, ei.element, profile);
          } else {
//            checkNonPrimitive(appContext, errors, ei.path, type, ei.definition, ei.element, profile);
            if (type.equals("Identifier"))
              checkIdentifier(errors, ei.path, ei.element, ei.definition);
            else if (type.equals("Coding"))
              checkCoding(errors, ei.path, ei.element, profile, ei.definition, inCodeableConcept);
            else if (type.equals("CodeableConcept")) {
              checkCodeableConcept(errors, ei.path, ei.element, profile, ei.definition);
              thisIsCodeableConcept = true;
            } else if (type.equals("Reference"))
              checkReference(hostContext, errors, ei.path, ei.element, profile, ei.definition, actualType, localStack);
            // We only check extensions if we're not in a complex extension or if the element we're dealing with is not defined as part of that complex extension
            if (type.equals("Extension") && ei.element.getChildValue("url").contains("/"))
              checkExtension(hostContext, errors, ei.path, resource, ei.element, ei.definition, profile, localStack);
            else if (type.equals("Resource"))
              validateContains(hostContext, errors, ei.path, ei.definition, definition, resource, ei.element, localStack, idStatusForEntry(element, ei)); // if
            // (str.matches(".*([.,/])work\\1$"))
            else {
              StructureDefinition p = null;
              boolean elementValidated = false;
              if (profiles.isEmpty()) {
                p = getProfileForType(type);
                rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, p != null, "Unknown type " + type);
              } else if (profiles.size()==1) {
                p = this.context.fetchResource(StructureDefinition.class, profiles.get(0));
                rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, p != null, "Unknown profile " + profiles.get(0));
              } else {
                elementValidated = true;
                HashMap<String, List<ValidationMessage>> goodProfiles = new HashMap<String, List<ValidationMessage>>();
                List<List<ValidationMessage>> badProfiles = new ArrayList<List<ValidationMessage>>();
                for (String typeProfile : profiles) {
                  p = this.context.fetchResource(StructureDefinition.class, typeProfile);
                  if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, p != null, "Unknown profile " + typeProfile)) {
                    List<ValidationMessage> profileErrors = new ArrayList<ValidationMessage>();
                    validateElement(hostContext, profileErrors, p, p.getSnapshot().getElement().get(0), profile, ei.definition, resource, ei.element, type, localStack, thisIsCodeableConcept);
                    boolean hasError = false;
                    for (ValidationMessage msg : profileErrors) {
                      if (msg.getLevel()==ValidationMessage.IssueSeverity.ERROR || msg.getLevel()==ValidationMessage.IssueSeverity.FATAL) {
                        hasError = true;
                        break;
                      }
                    }
                    if (hasError)
                      badProfiles.add(profileErrors);
                    else
                      goodProfiles.put(typeProfile, profileErrors);
                  }
                  if (goodProfiles.size()==1) {
                    /* TODO: this was goodProfiless.get(0) which will always
                     * return null - Is the first value what was wanted? */
                    errors.addAll(goodProfiles.values().iterator().next());
                  } else if (goodProfiles.size()==0) {
                    rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, false, "Unable to find matching profile among choices: " + StringUtils.join("; ", profiles));
                    for (List<ValidationMessage> messages : badProfiles) {
                      errors.addAll(messages);
                    }
                  } else {
                    warning(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, false, "Found multiple matching profiles among choices: " + StringUtils.join("; ", goodProfiles.keySet()));
                    for (List<ValidationMessage> messages : goodProfiles.values()) {
                      errors.addAll(messages);
                    }                    
                  }
                }
              }
              if (p!=null) {
                if (!elementValidated)
                  validateElement(hostContext, errors, p, p.getSnapshot().getElement().get(0), profile, ei.definition, resource, ei.element, type, localStack, thisIsCodeableConcept);
                int index = profile.getSnapshot().getElement().indexOf(ei.definition);
                if (index < profile.getSnapshot().getElement().size() - 1) {
                  String nextPath = profile.getSnapshot().getElement().get(index+1).getPath();
                  if (!nextPath.equals(ei.definition.getPath()) && nextPath.startsWith(ei.definition.getPath()))
                    validateElement(hostContext, errors, profile, ei.definition, null, null, resource, ei.element, type, localStack, thisIsCodeableConcept);
                }
              }
            }
          }
        } else {
          if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), ei.definition != null, "Unrecognised Content " + ei.name))
            validateElement(hostContext, errors, profile, ei.definition, null, null, resource, ei.element, type, localStack, false);
        }
      }
    }
  }

  private IdStatus idStatusForEntry(Element ep, ElementInfo ei) {
    if (isBundleEntry(ei.path)) {
      Element req = ep.getNamedChild("request");
      Element resp = ep.getNamedChild("response");
      Element fullUrl = ep.getNamedChild("fullUrl");
      Element method = null;
      Element url = null;
      if (req != null) {
        method = req.getNamedChild("method");
        url = req.getNamedChild("url");
      }
      if (resp != null) {
        return IdStatus.OPTIONAL;
      } if (method == null) {
        if (fullUrl == null)
          return IdStatus.REQUIRED;
        else if (fullUrl.primitiveValue().startsWith("urn:uuid:") || fullUrl.primitiveValue().startsWith("urn:oid:"))
          return IdStatus.OPTIONAL;
        else
          return IdStatus.REQUIRED;
      } else {
        String s = method.primitiveValue();
        if (s.equals("PUT")) {
          if (url == null)
            return IdStatus.REQUIRED;
          else
            return IdStatus.OPTIONAL; // or maybe prohibited? not clear
        } else if (s.equals("POST"))
          return IdStatus.OPTIONAL; // this should be prohibited, but see task 9102
        else // actually, we should never get to here; a bundle entry with method get/delete should not have a resource
          return IdStatus.OPTIONAL;					
      }
    } else if (isParametersEntry(ei.path) || isBundleOutcome(ei.path))
      return IdStatus.OPTIONAL; 
    else
      return IdStatus.REQUIRED; 
  }

  private void checkInvariants(ValidatorHostContext hostContext, List<ValidationMessage> errors, String path, StructureDefinition profile, ElementDefinition ed, String typename, String typeProfile, Element resource, Element element) throws FHIRException, FHIRException {
    if (noInvariantChecks)
      return;

    for (ElementDefinitionConstraintComponent inv : ed.getConstraint()) {
      if (inv.hasExpression()) {
        ExpressionNode n = (ExpressionNode) inv.getUserData("validator.expression.cache");
        if (n == null) {
          long t = System.nanoTime();
          try {
            n = fpe.parse(inv.getExpression());
          } catch (FHIRLexerException e) {
            throw new FHIRException("Problem processing expression "+inv.getExpression() +" in profile " + profile.getUrl() + " path " + path + ": " + e.getMessage());
          }
          fpeTime = fpeTime + (System.nanoTime() - t);
          inv.setUserData("validator.expression.cache", n);
        }

        String msg;
        boolean ok;
        try {
          long t = System.nanoTime();
          ok = fpe.evaluateToBoolean(hostContext, resource, element, n);
          fpeTime = fpeTime + (System.nanoTime() - t);
          msg = fpe.forLog();
        } catch (Exception ex) {
          ok = false;
          msg = ex.getMessage(); 
        }
        if (!ok) {
          try {
            ok = fpe.evaluateToBoolean(hostContext, resource, element, n);
          } catch (PathEngineException e) {
            throw new FHIRException("Problem processing expression "+inv.getExpression() +" in profile " + profile.getUrl() + " path " + path + ": " + e.getMessage());
          }
          if (!Utilities.noString(msg))
            msg = " ("+msg+")";
          if (inv.getSeverity() == ConstraintSeverity.ERROR)
            rule(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getHuman()+msg+" ["+inv.getExpression()+"]");
          else if (inv.getSeverity() == ConstraintSeverity.WARNING)
            warning(errors, IssueType.INVARIANT, element.line(), element.line(), path, ok, inv.getHuman()+msg+" ["+inv.getExpression()+"]");
        }
      }
    }
  }

  private void validateMessage(List<ValidationMessage> errors, List<Element> entries, Element messageHeader, NodeStack stack, String fullUrl, String id) {
    // first entry must be a messageheader
    if (rule(errors, IssueType.INVALID, messageHeader.line(), messageHeader.col(), stack.getLiteralPath(), messageHeader.getType().equals("MessageHeader"),
        "The first entry in a message must be a MessageHeader")) {
      // the composition subject and section references must resolve in the bundle
      List<Element> elements = messageHeader.getChildren("data");
      for (Element elem: elements)
        validateBundleReference(errors, entries, elem, "MessageHeader Data", stack.push(elem, -1, null, null), fullUrl, "MessageHeader", id);
    }
  }

  private void validateObservation(List<ValidationMessage> errors, Element element, NodeStack stack) {
    // all observations should have a subject, a performer, and a time

    bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("subject") != null, "All observations should have a subject");
    bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("performer") != null, "All observations should have a performer");
    bpCheck(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), element.getNamedChild("effectiveDateTime") != null || element.getNamedChild("effectivePeriod") != null,
        "All observations should have an effectiveDateTime or an effectivePeriod");
  }

  /*
   * The actual base entry point
   */
  /*  private void validateResource(List<ValidationMessage> errors, Element resource, Element element, StructureDefinition defn, ValidationProfileSet profiles, IdStatus idstatus, NodeStack stack) throws FHIRException, FHIRException {
    List<StructureDefinition> declProfiles = new ArrayList<StructureDefinition>();
    List<Element> meta = element.getChildrenByName("meta");
    if (!meta.isEmpty()) {
      for (Element profileName : meta.get(0).getChildrenByName("profile")) {
        StructureDefinition sd = context.fetchResource(StructureDefinition.class, profileName.getValue());
        if (sd != null)
          declProfiles.add(sd);
      }
    }

    if (!declProfiles.isEmpty()) {
      // Validate against profiles rather than the resource itself as they'll be more constrained and will cover the resource elements anyhow
      for (StructureDefinition sd : declProfiles)
        validateResource2(errors, resource, element, sd, profiles, idstatus, stack);
    } else
      validateResource2(errors, resource, element, defn, profiles, idstatus, stack);
  }*/

  private void validateResource(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element resource, Element element, StructureDefinition defn, ValidationProfileSet profiles, IdStatus idstatus, NodeStack stack, boolean isEntry) throws FHIRException, FHIRException, IOException {
    assert stack != null;
    assert resource != null;

    boolean ok = true;

    String resourceName = element.getType(); // todo: consider namespace...?
    if (defn == null) {
      long t = System.nanoTime();
      defn = element.getProperty().getStructure();
      if (defn == null)
        defn = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName);
      if (profiles!=null)
        getResourceProfiles(resource, stack).addProfiles(errors, profiles, stack.getLiteralPath(), element, isEntry);
      sdTime = sdTime + (System.nanoTime() - t);
      ok = rule(errors, IssueType.INVALID, element.line(), element.col(), stack.addToLiteralPath(resourceName), defn != null, "No definition found for resource type '" + resourceName + "'");
    }

    String type = defn.getKind() == StructureDefinitionKind.LOGICAL ? defn.getId() : defn.getType();
    // special case: we have a bundle, and the profile is not for a bundle. We'll try the first entry instead 
    if (!type.equals(resourceName) && resourceName.equals("Bundle")) {
      Element first = getFirstEntry(element);
      if (first != null && first.getType().equals(type)) {
        element = first;
        resourceName = element.getType();
        idstatus = IdStatus.OPTIONAL; // why?
      }
    }
    ok = rule(errors, IssueType.INVALID, -1, -1, stack.getLiteralPath(), type.equals(resourceName), "Specified profile type was '" + type + "', but found type '" + resourceName + "'");

    if (ok) {
      if (idstatus == IdStatus.REQUIRED && (element.getNamedChild("id") == null))
        rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), false, "Resource requires an id, but none is present");
      else if (idstatus == IdStatus.PROHIBITED && (element.getNamedChild("id") != null))
        rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), false, "Resource has an id, but none is allowed");
      start(hostContext, errors, resource, element, defn, stack); // root is both definition and type
    }
  }

  private void loadProfiles(ValidationProfileSet profiles) throws DefinitionException {
    if (profiles != null) { 
      for (String profile : profiles.getCanonicalUrls()) {
        StructureDefinition p = context.fetchResource(StructureDefinition.class, profile);
        if (p == null)
          throw new DefinitionException("StructureDefinition '" + profile + "' not found");
        profiles.getDefinitions().add(p);
      }
    }
  }

  private Element getFirstEntry(Element bundle) {
    List<Element> list = new ArrayList<Element>();
    bundle.getNamedChildren("entry", list);
    if (list.isEmpty())
      return null;
    Element resource = list.get(0).getNamedChild("resource");
    if (resource == null)
      return null;
    else
      return resource;
  }

  private void validateSections(List<ValidationMessage> errors, List<Element> entries, Element focus, NodeStack stack, String fullUrl, String id) {
    List<Element> sections = new ArrayList<Element>();
    focus.getNamedChildren("entry", sections);
    int i = 0;
    for (Element section : sections) {
      NodeStack localStack = stack.push(section, 1, null, null);
      validateBundleReference(errors, entries, section.getNamedChild("content"), "Section Content", localStack, fullUrl, "Composition", id);
      validateSections(errors, entries, section, localStack, fullUrl, id);
      i++;
    }
  }

  private boolean valueMatchesCriteria(Element value, ElementDefinition criteria) throws FHIRException {
    if (criteria.hasFixed()) {
      List<ValidationMessage> msgs = new ArrayList<ValidationMessage>();
      checkFixedValue(msgs, "{virtual}", value, criteria.getFixed(), "value", null);
      return msgs.size() == 0;
    } else if (criteria.hasBinding() && criteria.getBinding().getStrength() == BindingStrength.REQUIRED && criteria.getBinding().hasValueSet()) {
      throw new FHIRException("Unable to resolve slice matching - slice matching by value set not done");      
    } else {
      throw new FHIRException("Unable to resolve slice matching - no fixed value or required value set");
    }
  }

  private boolean yearIsValid(String v) {
    if (v == null) {
      return false;
    }
    try {
      int i = Integer.parseInt(v.substring(0, Math.min(4, v.length())));
      return i >= 1800 && i <= 2100;
    } catch (NumberFormatException e) {
      return false;
    }
  }

  public class ChildIterator {
    private String basePath;
    private Element parent;
    private int cursor;
    private int lastCount;

    public ChildIterator(String path, Element element) {
      parent = element;
      basePath = path;
      cursor = -1;
    }

    public int count() {
      String nb = cursor == 0 ? "--" : parent.getChildren().get(cursor-1).getName();
      String na = cursor >= parent.getChildren().size() - 1 ? "--" : parent.getChildren().get(cursor+1).getName();
      if (name().equals(nb) || name().equals(na) ) {
        return lastCount + 1;
      } else
        return -1;
    }

    public Element element() {
      return parent.getChildren().get(cursor);
    }

    public String name() {
      return element().getName();
    }

    public boolean next() {
      if (cursor == -1) {
        cursor++;
        lastCount = 0;
      } else {
        String lastName = name();
        cursor++;
        if (cursor < parent.getChildren().size() && name().equals(lastName))
          lastCount++;
        else
          lastCount = 0;
      }
      return cursor < parent.getChildren().size();
    }

    public String path() {
      int i = count();
      String sfx = "";
      if (i > -1) {
        sfx = "[" + Integer.toString(lastCount + 1) + "]";
      }
      return basePath + "." + name() + sfx;
    }
  }

  public class NodeStack {
    private ElementDefinition definition;
    private Element element;
    private ElementDefinition extension;
    private String literalPath; // xpath format
    private List<String> logicalPaths; // dotted format, various entry points
    private NodeStack parent;
    private ElementDefinition type;

    public NodeStack() {
    }	  

    public NodeStack(Element element) {
      this.element = element;
      literalPath = element.getName();
    }	  

    public String addToLiteralPath(String... path) {
      StringBuilder b = new StringBuilder();
      b.append(getLiteralPath());
      for (String p : path) {
        if (p.startsWith(":")) {
          b.append("[");
          b.append(p.substring(1));
          b.append("]");
        } else {
          b.append(".");
          b.append(p);
        }	  
      }	  
      return b.toString();
    }

    private ElementDefinition getDefinition() {
      return definition;
    }

    private Element getElement() {
      return element;
    }

    protected String getLiteralPath() {
      return literalPath == null ? "" : literalPath;
    }

    private List<String> getLogicalPaths() {
      return logicalPaths == null ? new ArrayList<String>() : logicalPaths;
    }

    private ElementDefinition getType() {
      return type;
    }

    private NodeStack push(Element element, int count, ElementDefinition definition, ElementDefinition type) {
      NodeStack res = new NodeStack();
      res.parent = this;
      res.element = element;
      res.definition = definition;
      res.literalPath = getLiteralPath() + "." + element.getName();
      if (count > -1)
        res.literalPath = res.literalPath + "[" + Integer.toString(count) + "]";
      res.logicalPaths = new ArrayList<String>();
      if (type != null) {
        // type will be bull if we on a stitching point of a contained resource, or if....
        res.type = type;
        String t = tail(definition.getPath());
        for (String lp : getLogicalPaths()) {
          res.logicalPaths.add(lp + "." + t);
          if (t.endsWith("[x]"))
            res.logicalPaths.add(lp + "." + t.substring(0, t.length() - 3) + type.getPath());
        }
        res.logicalPaths.add(type.getPath());
      } else if (definition != null) {
        for (String lp : getLogicalPaths())
          res.logicalPaths.add(lp + "." + element.getName());
      } else
        res.logicalPaths.addAll(getLogicalPaths());
      // CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
      // for (String lp : res.logicalPaths)
      // b.append(lp);
      // System.out.println(res.literalPath+" : "+b.toString());
      return res;
    }

    private void setType(ElementDefinition type) {
      this.type = type;
    }
  }

  private void checkForProcessingInstruction(List<ValidationMessage> errors, Document document) {
    Node node = document.getFirstChild();
    while (node != null) {
      rule(errors, IssueType.INVALID, -1, -1, "(document)", node.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE, "No processing instructions allowed in resources");
      node = node.getNextSibling();
    }
  }

  public class ElementInfo {

    public int index; // order of definition in overall order. all slices get the index of the slicing definition
    public int sliceindex; // order of the definition in the slices (if slice != null)
    public int count;
    public ElementDefinition definition;
    public ElementDefinition slice;
    private Element element;
    private String name;
    private String path;

    public ElementInfo(String name, Element element, String path, int count) {
      this.name = name;
      this.element = element;
      this.path = path;
      this.count = count;
    }

    public int col() {
      return element.col();
    }

    public int line() {
      return element.line();
    }

  }

  public String reportTimes() {
    String s = String.format("Times: overall = %d, tx = %d, sd = %d, load = %d, fpe = %d", overall, txTime, sdTime, loadTime, fpeTime);
    overall = 0;
    txTime = 0;
    sdTime = 0;
    loadTime = 0;
    fpeTime = 0;
    return s;
  }

  public boolean isNoBindingMsgSuppressed() {
    return noBindingMsgSuppressed;
  }

  public IResourceValidator setNoBindingMsgSuppressed(boolean noBindingMsgSuppressed) {
    this.noBindingMsgSuppressed = noBindingMsgSuppressed;
    return this;
  }

  
  public boolean isNoTerminologyChecks() {
    return noTerminologyChecks;
  }

  public IResourceValidator setNoTerminologyChecks(boolean noTerminologyChecks) {
    this.noTerminologyChecks = noTerminologyChecks;
    return this;
  }

  public void checkAllInvariants(){
    for (StructureDefinition sd : context.allStructures()) {
      if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) {
        for (ElementDefinition ed : sd.getSnapshot().getElement()) {
          for (ElementDefinitionConstraintComponent inv : ed.getConstraint()) {
            if (inv.hasExpression()) {
              try {
                ExpressionNode n = (ExpressionNode) inv.getUserData("validator.expression.cache");
                if (n == null) {
                  n = fpe.parse(inv.getExpression());
                  inv.setUserData("validator.expression.cache", n);
                }
                fpe.check(null, sd.getKind() == StructureDefinitionKind.RESOURCE ?  sd.getType() : "DomainResource", ed.getPath(), n);
              } catch (Exception e) {
                System.out.println("Error processing structure ["+sd.getId()+"] path "+ed.getPath()+":"+inv.getKey()+" (\""+inv.getExpression()+"\"): "+e.getMessage());
              }
            }
          }
        }
      }
    }
  }

}
