@Override
  protected void handleControlEnd(
      String uri,
      String localname,
      String qName,
      Attributes attributes,
      String staticId,
      String effectiveId,
      XFormsControl control)
      throws SAXException {

    handlerContext.popCaseContext();

    final ElementHandlerController controller = handlerContext.getController();
    if (!handlerContext.isNoScript()) {
      currentOutputInterceptor.flushCharacters(true, true);

      // Restore output
      controller.setOutput(currentSavedOutput);

      final boolean isMustGenerateBeginEndDelimiters =
          !handlerContext.isFullUpdateTopLevelControl(effectiveId);
      if (isMustGenerateBeginEndDelimiters) {
        if (currentOutputInterceptor.getDelimiterNamespaceURI() != null) {
          // Output end delimiter
          currentOutputInterceptor.outputDelimiter(
              currentSavedOutput,
              currentOutputInterceptor.getDelimiterNamespaceURI(),
              currentOutputInterceptor.getDelimiterPrefix(),
              currentOutputInterceptor.getDelimiterLocalName(),
              "xforms-case-begin-end",
              "xforms-case-end-" + XFormsUtils.namespaceId(containingDocument, effectiveId));
        } else {
          // Output start and end delimiter using xhtml:span
          final String xhtmlPrefix = handlerContext.findXHTMLPrefix();
          currentOutputInterceptor.outputDelimiter(
              currentSavedOutput,
              XMLConstants.XHTML_NAMESPACE_URI,
              xhtmlPrefix,
              "span",
              "xforms-case-begin-end",
              "xforms-case-begin-" + XFormsUtils.namespaceId(containingDocument, effectiveId));
          currentOutputInterceptor.outputDelimiter(
              currentSavedOutput,
              XMLConstants.XHTML_NAMESPACE_URI,
              xhtmlPrefix,
              "span",
              "xforms-case-begin-end",
              "xforms-case-end-" + XFormsUtils.namespaceId(containingDocument, effectiveId));
        }
      }
    } else if (!isVisible) {
      // Case not visible, restore output
      controller.setOutput(currentSavedOutput);
    }
  }
  @Override
  public void handleControlEnd(
      String uri,
      String localname,
      String qName,
      Attributes attributes,
      String effectiveId,
      XFormsControl control)
      throws SAXException {

    final ElementHandlerController controller = handlerContext.getController();
    if (!handlerContext.isNoScript()) {
      // Restore output
      controller.setOutput(currentSavedOutput);

      // Delimiter: end repeat
      outputInterceptor.flushCharacters(true, true);

      final boolean isMustGenerateBeginEndDelimiters =
          !handlerContext.isFullUpdateTopLevelControl(effectiveId);
      if (isMustGenerateBeginEndDelimiters) {
        outputInterceptor.outputDelimiter(
            currentSavedOutput,
            outputInterceptor.getDelimiterNamespaceURI(),
            outputInterceptor.getDelimiterPrefix(),
            outputInterceptor.getDelimiterLocalName(),
            "xforms-group-begin-end",
            "group-end-" + XFormsUtils.namespaceId(containingDocument, effectiveId));
      }
    } else if (isNonRelevant(control)) {
      // In noscript, group was not visible, restore output
      controller.setOutput(currentSavedOutput);
    }

    // Don't support help, alert, or hint!
  }
  public void handleControlStart(
      String uri,
      String localname,
      String qName,
      Attributes attributes,
      final String effectiveId,
      XFormsControl control)
      throws SAXException {

    final String groupElementName = getContainingElementName();
    final String xhtmlPrefix = handlerContext.findXHTMLPrefix();
    final String groupElementQName = XMLUtils.buildQName(xhtmlPrefix, groupElementName);

    final ElementHandlerController controller = handlerContext.getController();

    // Place interceptor on output

    // NOTE: Strictly, we should be able to do without the interceptor. We use it here because it
    // automatically handles ids and element names
    currentSavedOutput = controller.getOutput();
    if (!handlerContext.isNoScript()) {

      final boolean isMustGenerateBeginEndDelimiters =
          !handlerContext.isFullUpdateTopLevelControl(effectiveId);

      // Classes on top-level elements and characters and on the first delimiter
      final String elementClasses;
      {
        final StringBuilder classes = new StringBuilder();
        appendControlUserClasses(attributes, control, classes);
        // NOTE: Could also use getInitialClasses(uri, localname, attributes, control), but then we
        // get the
        // xforms-group-appearance-xxforms-separator class. Is that desirable?
        handleMIPClasses(
            classes,
            getPrefixedId(),
            control); // as of August 2009, actually only need the marker class as well as
                      // xforms-disabled if the group is non-relevant
        elementClasses = classes.toString();
      }

      outputInterceptor =
          new OutputInterceptor(
              currentSavedOutput,
              groupElementQName,
              new OutputInterceptor.Listener() {

                // Classes on first delimiter
                private final String firstDelimiterClasses;

                {
                  final StringBuilder classes = new StringBuilder("xforms-group-begin-end");
                  if (elementClasses.length() > 0) {
                    classes.append(' ');
                    classes.append(elementClasses);
                  }
                  firstDelimiterClasses = classes.toString();
                }

                public void generateFirstDelimiter(OutputInterceptor outputInterceptor)
                    throws SAXException {
                  // Delimiter: begin group
                  if (isMustGenerateBeginEndDelimiters) {
                    outputInterceptor.outputDelimiter(
                        currentSavedOutput,
                        outputInterceptor.getDelimiterNamespaceURI(),
                        outputInterceptor.getDelimiterPrefix(),
                        outputInterceptor.getDelimiterLocalName(),
                        firstDelimiterClasses,
                        "group-begin-" + XFormsUtils.namespaceId(containingDocument, effectiveId));
                  }
                }
              });

      controller.setOutput(new DeferredXMLReceiverImpl(outputInterceptor));

      // Set control classes
      outputInterceptor.setAddedClasses(elementClasses);
    } else if (isNonRelevant(control)) {
      // In noscript, if the group not visible, set output to a black hole
      controller.setOutput(new DeferredXMLReceiverAdapter());
    }

    // Don't support label, help, alert, or hint and other appearances, only the content!
  }
  public static void outputResponseDocument(
      final PipelineContext pipelineContext,
      final ExternalContext externalContext,
      final IndentedLogger indentedLogger,
      final SAXStore annotatedDocument,
      final XFormsContainingDocument containingDocument,
      final XMLReceiver xmlReceiver)
      throws SAXException, IOException {

    final List<XFormsContainingDocument.Load> loads = containingDocument.getLoadsToRun();
    if (containingDocument.isGotSubmissionReplaceAll()) {
      // 1. Got a submission with replace="all"

      // NOP: Response already sent out by a submission
      // TODO: modify XFormsModelSubmission accordingly
      indentedLogger.logDebug("", "handling response for submission with replace=\"all\"");
    } else if (loads != null && loads.size() > 0) {
      // 2. Got at least one xforms:load

      // Send redirect out

      // Get first load only
      final XFormsContainingDocument.Load load = loads.get(0);

      // Send redirect
      final String redirectResource = load.getResource();
      indentedLogger.logDebug(
          "", "handling redirect response for xforms:load", "url", redirectResource);
      // Set isNoRewrite to true, because the resource is either a relative path or already contains
      // the servlet context
      externalContext.getResponse().sendRedirect(redirectResource, null, false, false, true);

      // Still send out a null document to signal that no further processing must take place
      XMLUtils.streamNullDocument(xmlReceiver);
    } else {
      // 3. Regular case: produce an XHTML document out

      final ElementHandlerController controller = new ElementHandlerController();

      // Register handlers on controller (the other handlers are registered by the body handler)
      {
        controller.registerHandler(
            XHTMLHeadHandler.class.getName(), XMLConstants.XHTML_NAMESPACE_URI, "head");
        controller.registerHandler(
            XHTMLBodyHandler.class.getName(), XMLConstants.XHTML_NAMESPACE_URI, "body");

        // Register a handler for AVTs on HTML elements
        final boolean hostLanguageAVTs =
            XFormsProperties
                .isHostLanguageAVTs(); // TODO: this should be obtained per document, but we only
        // know about this in the extractor
        if (hostLanguageAVTs) {
          controller.registerHandler(
              XXFormsAttributeHandler.class.getName(),
              XFormsConstants.XXFORMS_NAMESPACE_URI,
              "attribute");
          controller.registerHandler(
              XHTMLElementHandler.class.getName(), XMLConstants.XHTML_NAMESPACE_URI);
        }

        // Swallow XForms elements that are unknown
        controller.registerHandler(
            NullHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI);
        controller.registerHandler(
            NullHandler.class.getName(), XFormsConstants.XXFORMS_NAMESPACE_URI);
        controller.registerHandler(NullHandler.class.getName(), XFormsConstants.XBL_NAMESPACE_URI);
      }

      // Set final output
      controller.setOutput(new DeferredXMLReceiverImpl(xmlReceiver));
      // Set handler context
      controller.setElementHandlerContext(
          new HandlerContext(
              controller, pipelineContext, containingDocument, externalContext, null));
      // Process the entire input
      annotatedDocument.replay(
          new ExceptionWrapperXMLReceiver(controller, "converting XHTML+XForms document to XHTML"));
    }

    containingDocument.afterInitialResponse();
  }
  @Override
  protected void handleControlStart(
      String uri,
      String localname,
      String qName,
      Attributes attributes,
      String staticId,
      final String effectiveId,
      XFormsControl control)
      throws SAXException {

    final String xhtmlPrefix = handlerContext.findXHTMLPrefix();
    final String spanQName = XMLUtils.buildQName(xhtmlPrefix, "span");

    // Determine whether this case is visible
    final XFormsCaseControl caseControl =
        (XFormsCaseControl) containingDocument.getControls().getObjectByEffectiveId(effectiveId);
    if (!handlerContext.isTemplate() && caseControl != null) {
      // This case is visible if it is selected or if the switch is read-only and we display
      // read-only as static
      isVisible = caseControl.isVisible();
    } else {
      isVisible = false;
    }

    final ElementHandlerController controller = handlerContext.getController();
    currentSavedOutput = controller.getOutput();

    // Place interceptor if needed
    if (!handlerContext.isNoScript()) {

      final boolean isMustGenerateBeginEndDelimiters =
          !handlerContext.isFullUpdateTopLevelControl(effectiveId);

      // Classes on top-level elements and characters and on the first delimiter
      final String elementClasses;
      {
        final StringBuilder classes = new StringBuilder();
        appendControlUserClasses(attributes, control, classes);
        // Don't add MIP classes as they can conflict with classes of nested content if used outside
        // <tr>, etc.
        elementClasses = classes.toString();
      }

      currentOutputInterceptor =
          new OutputInterceptor(
              currentSavedOutput,
              spanQName,
              new OutputInterceptor.Listener() {

                // Classes on first delimiter
                private final String firstDelimiterClasses;

                {
                  final StringBuilder classes = new StringBuilder("xforms-case-begin-end");
                  if (elementClasses.length() > 0) {
                    classes.append(' ');
                    classes.append(elementClasses);
                  }
                  firstDelimiterClasses = classes.toString();
                }

                public void generateFirstDelimiter(OutputInterceptor outputInterceptor)
                    throws SAXException {
                  if (isMustGenerateBeginEndDelimiters) {
                    // Delimiter: begin case
                    outputInterceptor.outputDelimiter(
                        currentSavedOutput,
                        outputInterceptor.getDelimiterNamespaceURI(),
                        outputInterceptor.getDelimiterPrefix(),
                        outputInterceptor.getDelimiterLocalName(),
                        firstDelimiterClasses,
                        "xforms-case-begin-"
                            + XFormsUtils.namespaceId(containingDocument, effectiveId));
                  }
                }
              });

      final String controlClasses;
      {
        final StringBuilder classes =
            new StringBuilder(isVisible ? "xforms-case-selected" : "xforms-case-deselected");
        if (elementClasses.length() > 0) {
          classes.append(' ');
          classes.append(elementClasses);
        }
        controlClasses = classes.toString();
      }

      currentOutputInterceptor.setAddedClasses(controlClasses);

      controller.setOutput(new DeferredXMLReceiverImpl(currentOutputInterceptor));
    } else if (!isVisible) {
      // Case not visible, set output to a black hole
      controller.setOutput(new DeferredXMLReceiverAdapter());
    }

    handlerContext.pushCaseContext(isVisible);
  }