@Override
  public void outputAjaxDiff(
      PipelineContext pipelineContext,
      ContentHandlerHelper ch,
      XFormsControl other,
      AttributesImpl attributesImpl,
      boolean isNewlyVisibleSubtree) {
    // Output regular diff
    super.outputAjaxDiff(pipelineContext, ch, other, attributesImpl, isNewlyVisibleSubtree);

    // Output itemset diff
    if (mustSendItemsetUpdate(pipelineContext, (XFormsSelect1Control) other)) {
      ch.startElement(
          "xxf",
          XFormsConstants.XXFORMS_NAMESPACE_URI,
          "itemset",
          new String[] {"id", XFormsUtils.namespaceId(containingDocument, getEffectiveId())});
      {
        final Itemset itemset = getItemset(pipelineContext);
        if (itemset != null) {
          final String result =
              itemset.getJSONTreeInfo(pipelineContext, null, false, getLocationData());
          if (result.length() > 0) ch.text(result);
        }
      }
      ch.endElement();
    }
  }
 private boolean mustSendItemsetUpdate(
     PropertyContext propertyContext, XFormsSelect1Control otherSelect1Control) {
   if (getSelectionControl().hasStaticItemset()) {
     // There is no need to send an update:
     //
     // 1. Items are static...
     // 2. ...and they have been outputted statically in the HTML page, directly or in repeat
     // template
     return false;
   } else if (isStaticReadonly()) {
     // There is no need to send an update for static readonly controls
     return false;
   } else {
     // There is a possible change
     if (XFormsSingleNodeControl.isRelevant(otherSelect1Control) != isRelevant()) {
       // Relevance changed
       // Here we decide to send an update only if we become relevant, as the client will know that
       // the
       // new state of the control is non-relevant and can handle the itemset on the client as it
       // wants.
       return isRelevant();
     } else if (!XFormsSingleNodeControl.isRelevant(this)) {
       // We were and are non-relevant, no update
       return false;
     } else {
       // If the itemsets changed, then we need to send an update
       // NOTE: This also covers the case where the control was and is non-relevant
       return !Itemset.compareItemsets(
           otherSelect1Control.getItemset(propertyContext), getItemset(propertyContext));
     }
   }
 }
 private void outputJSONTreeInfo(
     XFormsValueControl valueControl, Itemset itemset, boolean many, ContentHandler contentHandler)
     throws SAXException {
   if (valueControl != null && !handlerContext.isTemplate()) {
     // Produce a JSON fragment with hierarchical information
     final String result =
         itemset.getJSONTreeInfo(
             pipelineContext,
             valueControl.getValue(pipelineContext),
             many,
             handlerContext.getLocationData());
     contentHandler.characters(result.toCharArray(), 0, result.length());
   } else {
     // Don't produce any content when generating a template
   }
 }
  @Override
  public void storeExternalValue(PropertyContext propertyContext, String value, String type) {

    if (!(this
        instanceof
        XFormsSelectControl)) { // kind of a HACK due to the way our class hierarchy is setup
      // Handle xforms:select1-specific logic

      // Decrypt incoming value. With open selection, values are sent to the client.
      if (isEncryptItemValues()) {
        try {
          value = XFormsItemUtils.decryptValue(propertyContext, value);
        } catch (IllegalArgumentException e) {
          getIndentedLogger()
              .logError(
                  "", "exception decrypting value", "control id", getEffectiveId(), "value", value);
          throw e;
        }
      }

      // Current control value
      final String controlValue = getValue(propertyContext);

      // Iterate over all the items
      final Itemset itemset = getItemset(propertyContext);
      final List<XFormsEvent> selectEvents = new ArrayList<XFormsEvent>();
      final List<XFormsEvent> deselectEvents = new ArrayList<XFormsEvent>();
      if (itemset != null) {
        for (Item currentItem : itemset.toList()) {
          final String currentItemValue = currentItem.getValue();
          final boolean itemWasSelected = controlValue.equals(currentItemValue);
          final boolean itemIsSelected;
          if (value.equals(currentItemValue)) {
            // Value is currently selected in the UI
            itemIsSelected = true;
          } else {
            // Value is currently NOT selected in the UI
            itemIsSelected = false;
          }

          // Handle xforms-select / xforms-deselect
          // TODO: Dispatch to itemset or item once we support doing that
          if (!itemWasSelected && itemIsSelected) {
            selectEvents.add(new XFormsSelectEvent(containingDocument, this, currentItemValue));
          } else if (itemWasSelected && !itemIsSelected) {
            deselectEvents.add(new XFormsDeselectEvent(containingDocument, this, currentItemValue));
          }
        }
      }

      // Dispatch xforms-deselect events
      if (deselectEvents.size() > 0) {
        for (XFormsEvent currentEvent : deselectEvents) {
          currentEvent
              .getTargetObject()
              .getXBLContainer(containingDocument)
              .dispatchEvent(propertyContext, currentEvent);
        }
      }
      // Select events must be sent after all xforms-deselect events
      final boolean hasSelectedItem = selectEvents.size() > 0;
      if (hasSelectedItem) {
        for (XFormsEvent currentEvent : selectEvents) {
          currentEvent
              .getTargetObject()
              .getXBLContainer(containingDocument)
              .dispatchEvent(propertyContext, currentEvent);
        }
      }

      if (hasSelectedItem || isOpenSelection()) {
        // Only then do we store the external value. This ensures that if the value is NOT in the
        // itemset AND
        // we are a closed selection then we do NOT store the value in instance.
        super.storeExternalValue(propertyContext, value, type);
      }
    } else {
      // Forward to superclass
      super.storeExternalValue(propertyContext, value, type);
    }
  }
  private void outputFull(
      String uri,
      String localname,
      Attributes attributes,
      String effectiveId,
      XFormsValueControl xformsControl,
      Itemset itemset,
      boolean isMultiple,
      boolean isBooleanInput)
      throws SAXException {
    final ContentHandler contentHandler = handlerContext.getController().getOutput();
    final AttributesImpl containerAttributes =
        getContainerAttributes(uri, localname, attributes, effectiveId, xformsControl, !isFull);
    final String xhtmlPrefix = handlerContext.findXHTMLPrefix();

    final String fullItemType = isMultiple ? "checkbox" : "radio";

    // In noscript mode, use <fieldset>

    // TODO: This really hasn't much to do with noscript; should we always use fieldset, or make
    // this an
    // option? Benefit of limiting to noscript is that then no JS change is needed
    final String containingElementName = handlerContext.isNoScript() ? "fieldset" : "span";
    final String containingElementQName = XMLUtils.buildQName(xhtmlPrefix, containingElementName);

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

      // Old layout always output container <span>/<fieldset>, and in new layout we only put it for
      // select/select1
      final boolean outputContainerElement = !isBooleanInput || !handlerContext.isSpanHTMLLayout();
      if (outputContainerElement)
        contentHandler.startElement(
            XMLConstants.XHTML_NAMESPACE_URI,
            containingElementName,
            containingElementQName,
            containerAttributes);
      {
        if (handlerContext.isNoScript()) {
          // Output <legend>
          final String legendName = "legend";
          final String legendQName = XMLUtils.buildQName(xhtmlPrefix, legendName);
          reusableAttributes.clear();
          // TODO: handle other attributes? xforms-disabled?
          reusableAttributes.addAttribute(
              "", "class", "class", ContentHandlerHelper.CDATA, "xforms-label");
          contentHandler.startElement(
              XMLConstants.XHTML_NAMESPACE_URI, legendName, legendQName, reusableAttributes);
          if (xformsControl != null) {
            final boolean mustOutputHTMLFragment = xformsControl.isHTMLLabel(pipelineContext);
            outputLabelText(
                contentHandler,
                xformsControl,
                xformsControl.getLabel(pipelineContext),
                xhtmlPrefix,
                mustOutputHTMLFragment);
          }
          contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, legendName, legendQName);
        }

        if (itemset != null) {
          int itemIndex = 0;
          for (Iterator<Item> i = itemset.toList().iterator(); i.hasNext(); itemIndex++) {
            final Item item = i.next();
            final String itemEffectiveId = getItemId(effectiveId, Integer.toString(itemIndex));
            handleItemFull(
                pipelineContext,
                handlerContext,
                contentHandler,
                reusableAttributes,
                attributes,
                xhtmlPrefix,
                spanQName,
                containingDocument,
                xformsControl,
                effectiveId,
                itemEffectiveId,
                isMultiple,
                fullItemType,
                item,
                itemIndex == 0);
          }
        }
      }
      if (outputContainerElement)
        contentHandler.endElement(
            XMLConstants.XHTML_NAMESPACE_URI, containingElementName, containingElementQName);
    }

    // NOTE: Templates for full items are output globally in XHTMLBodyHandler
  }
  public void outputContent(
      String uri,
      String localname,
      Attributes attributes,
      String effectiveId,
      final XFormsValueControl xformsSelect1Control,
      Itemset itemset,
      final boolean isMultiple,
      final boolean isFull,
      boolean isBooleanInput)
      throws SAXException {

    final ContentHandler contentHandler = handlerContext.getController().getOutput();

    final AttributesImpl containerAttributes =
        getContainerAttributes(
            uri, localname, attributes, effectiveId, xformsSelect1Control, !isFull);

    final String xhtmlPrefix = handlerContext.findXHTMLPrefix();
    if (!isStaticReadonly(xformsSelect1Control)) {
      if (isFull) {
        // Full appearance
        outputFull(
            uri,
            localname,
            attributes,
            effectiveId,
            xformsSelect1Control,
            itemset,
            isMultiple,
            isBooleanInput);
      } else {

        if (isOpenSelection) {

          if (isAutocomplete) {

            // Create xhtml:span
            final String spanQName = XMLUtils.buildQName(xhtmlPrefix, "span");
            contentHandler.startElement(
                XMLConstants.XHTML_NAMESPACE_URI, "span", spanQName, containerAttributes);

            {
              {
                // Create xhtml:input
                final String inputQName = XMLUtils.buildQName(xhtmlPrefix, "input");

                reusableAttributes.clear();
                reusableAttributes.addAttribute(
                    "", "type", "type", ContentHandlerHelper.CDATA, "text");
                reusableAttributes.addAttribute(
                    "",
                    "name",
                    "name",
                    ContentHandlerHelper.CDATA,
                    "xforms-select1-open-input-" + effectiveId);
                reusableAttributes.addAttribute(
                    "", "class", "class", ContentHandlerHelper.CDATA, "xforms-select1-open-input");
                reusableAttributes.addAttribute(
                    "", "autocomplete", "autocomplete", ContentHandlerHelper.CDATA, "off");

                final String value =
                    (xformsSelect1Control == null)
                        ? null
                        : xformsSelect1Control.getValue(pipelineContext);
                // NOTE: With open selection, we send all values to the client but not encrypt them
                // because the client matches on values
                reusableAttributes.addAttribute(
                    "", "value", "value", ContentHandlerHelper.CDATA, (value == null) ? "" : value);
                handleDisabledAttribute(
                    reusableAttributes, containingDocument, xformsSelect1Control);
                contentHandler.startElement(
                    XMLConstants.XHTML_NAMESPACE_URI, "input", inputQName, reusableAttributes);

                contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "input", inputQName);
              }
              {
                // Create xhtml:select
                final String selectQName = XMLUtils.buildQName(xhtmlPrefix, "select");

                reusableAttributes.clear();
                reusableAttributes.addAttribute(
                    "", "class", "class", ContentHandlerHelper.CDATA, "xforms-select1-open-select");

                if (isCompact)
                  reusableAttributes.addAttribute(
                      "", "multiple", "multiple", ContentHandlerHelper.CDATA, "multiple");

                // Handle accessibility attributes
                handleAccessibilityAttributes(attributes, reusableAttributes);

                contentHandler.startElement(
                    XMLConstants.XHTML_NAMESPACE_URI, "select", selectQName, reusableAttributes);

                final String optionQName = XMLUtils.buildQName(xhtmlPrefix, "option");
                handleItemCompact(
                    contentHandler,
                    optionQName,
                    xformsSelect1Control,
                    isMultiple,
                    EMPTY_TOP_LEVEL_ITEM);
                if (itemset != null) {
                  for (final Item item : itemset.toList()) {
                    if (item.getValue() != null)
                      handleItemCompact(
                          contentHandler, optionQName, xformsSelect1Control, isMultiple, item);
                  }
                }

                contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "select", selectQName);
              }
            }

            contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "span", spanQName);
          } else {
            // We do not support other appearances or regular open selection for now
            throw new ValidationException(
                "Open selection currently only supports the xxforms:autocomplete appearance.",
                new ExtendedLocationData(
                    handlerContext.getLocationData(),
                    "producing markup for xforms:" + localname + " control",
                    (xformsSelect1Control != null)
                        ? xformsSelect1Control.getControlElement()
                        : null));
          }

        } else if (isTree) {
          // xxforms:tree appearance

          // Create xhtml:div with tree info
          final String divQName = XMLUtils.buildQName(xhtmlPrefix, "div");

          handleDisabledAttribute(containerAttributes, containingDocument, xformsSelect1Control);
          contentHandler.startElement(
              XMLConstants.XHTML_NAMESPACE_URI, "div", divQName, containerAttributes);
          if (itemset != null) { // can be null if the control is non-relevant
            outputJSONTreeInfo(xformsSelect1Control, itemset, isMultiple, contentHandler);
          }
          contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "div", divQName);

        } else if (isMenu) {
          // xxforms:menu appearance

          // Create enclosing xhtml:div
          final String divQName = XMLUtils.buildQName(xhtmlPrefix, "div");
          final String ulQName = XMLUtils.buildQName(xhtmlPrefix, "ul");
          final String liQName = XMLUtils.buildQName(xhtmlPrefix, "li");
          final String aQName = XMLUtils.buildQName(xhtmlPrefix, "a");

          handleDisabledAttribute(containerAttributes, containingDocument, xformsSelect1Control);
          contentHandler.startElement(
              XMLConstants.XHTML_NAMESPACE_URI, "div", divQName, containerAttributes);
          if (itemset != null) { // can be null if the control is non-relevant
            // Create xhtml:div with initial menu entries
            {
              itemset.visit(
                  contentHandler,
                  new ItemsetListener() {

                    private boolean groupJustStarted = false;

                    public void startLevel(ContentHandler contentHandler, Item item)
                        throws SAXException {

                      final boolean isTopLevel = item == null;

                      reusableAttributes.clear();
                      final String divClasses = isTopLevel ? "yuimenubar" : "yuimenu";
                      reusableAttributes.addAttribute(
                          "", "class", "class", ContentHandlerHelper.CDATA, divClasses);
                      contentHandler.startElement(
                          XMLConstants.XHTML_NAMESPACE_URI, "div", divQName, reusableAttributes);

                      reusableAttributes.clear();
                      reusableAttributes.addAttribute(
                          "", "class", "class", ContentHandlerHelper.CDATA, "bd");
                      contentHandler.startElement(
                          XMLConstants.XHTML_NAMESPACE_URI, "div", divQName, reusableAttributes);

                      reusableAttributes.clear();
                      // NOTE: We just decide to put item classes on <ul>
                      final String classes =
                          isTopLevel ? "first-of-type" : getItemClasses(item, "first-of-type");
                      reusableAttributes.addAttribute(
                          "", "class", "class", ContentHandlerHelper.CDATA, classes);
                      contentHandler.startElement(
                          XMLConstants.XHTML_NAMESPACE_URI, "ul", ulQName, reusableAttributes);

                      groupJustStarted = true;
                    }

                    public void endLevel(ContentHandler contentHandler) throws SAXException {
                      contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "ul", ulQName);
                      contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "div", divQName);
                      contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "div", divQName);

                      groupJustStarted = false;
                    }

                    public void startItem(ContentHandler contentHandler, Item item, boolean first)
                        throws SAXException {

                      final String liClasses;
                      {
                        final StringBuilder sb =
                            new StringBuilder(item.isTopLevel() ? "yuimenubaritem" : "yuimenuitem");
                        if (groupJustStarted) sb.append(" first-of-type");
                        liClasses = getItemClasses(item, sb.toString());
                      }
                      reusableAttributes.clear();
                      reusableAttributes.addAttribute(
                          "", "class", "class", ContentHandlerHelper.CDATA, liClasses);
                      contentHandler.startElement(
                          XMLConstants.XHTML_NAMESPACE_URI, "li", liQName, reusableAttributes);

                      reusableAttributes.clear();
                      reusableAttributes.addAttribute(
                          "", "href", "href", ContentHandlerHelper.CDATA, "#");
                      contentHandler.startElement(
                          XMLConstants.XHTML_NAMESPACE_URI, "a", aQName, reusableAttributes);

                      final String text = item.getLabel();
                      contentHandler.characters(text.toCharArray(), 0, text.length());

                      contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "a", aQName);

                      groupJustStarted = false;
                    }

                    public void endItem(ContentHandler contentHandler) throws SAXException {
                      contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "li", liQName);

                      groupJustStarted = false;
                    }
                  });
            }

            // Create xhtml:div with tree info
            reusableAttributes.clear();
            reusableAttributes.addAttribute(
                "", "class", "class", ContentHandlerHelper.CDATA, "xforms-initially-hidden");

            contentHandler.startElement(
                XMLConstants.XHTML_NAMESPACE_URI, "div", divQName, reusableAttributes);
            if (itemset != null) { // can be null if the control is non-relevant
              outputJSONTreeInfo(xformsSelect1Control, itemset, isMultiple, contentHandler);
            }
            contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "div", divQName);
          }
          contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "div", divQName);

        } else {
          // Create xhtml:select
          final String selectQName = XMLUtils.buildQName(xhtmlPrefix, "select");
          containerAttributes.addAttribute(
              "",
              "name",
              "name",
              ContentHandlerHelper.CDATA,
              effectiveId); // necessary for noscript mode

          if (isCompact)
            containerAttributes.addAttribute(
                "", "multiple", "multiple", ContentHandlerHelper.CDATA, "multiple");

          // Handle accessibility attributes
          handleAccessibilityAttributes(attributes, containerAttributes);

          handleDisabledAttribute(containerAttributes, containingDocument, xformsSelect1Control);
          contentHandler.startElement(
              XMLConstants.XHTML_NAMESPACE_URI, "select", selectQName, containerAttributes);
          {
            final String optionQName = XMLUtils.buildQName(xhtmlPrefix, "option");
            final String optGroupQName = XMLUtils.buildQName(xhtmlPrefix, "optgroup");

            if (itemset != null) {

              // Work in progress for in-bounds/out-of-bounds
              //                        if (!((XFormsSelect1Control)
              // xformsControl).isInBounds(items)) {
              //                            // Control is out of bounds so add first item with out
              // of bound value to handle this
              //                            handleItemCompact(contentHandler, optionQName,
              // xformsControl, isMultiple,
              //                                    new
              // XFormsItemUtils.Item(XFormsProperties.isEncryptItemValues(containingDocument),
              //                                            Collections.EMPTY_LIST, "",
              // xformsControl.getValue(pipelineContext), 1));
              //                        }

              itemset.visit(
                  contentHandler,
                  new ItemsetListener() {

                    private int optgroupCount = 0;

                    public void startLevel(ContentHandler contentHandler, Item item)
                        throws SAXException {}

                    public void endLevel(ContentHandler contentHandler) throws SAXException {
                      if (optgroupCount-- > 0) {
                        // End xhtml:optgroup
                        contentHandler.endElement(
                            XMLConstants.XHTML_NAMESPACE_URI, "optgroup", optGroupQName);
                      }
                    }

                    public void startItem(ContentHandler contentHandler, Item item, boolean first)
                        throws SAXException {

                      final String label = item.getLabel();
                      final String value = item.getValue();

                      if (value == null) {
                        final String itemClasses = getItemClasses(item, null);
                        final AttributesImpl optGroupAttributes =
                            getAttributes(XMLUtils.EMPTY_ATTRIBUTES, itemClasses, null);
                        if (label != null)
                          optGroupAttributes.addAttribute(
                              "", "label", "label", ContentHandlerHelper.CDATA, label);

                        // Start xhtml:optgroup
                        contentHandler.startElement(
                            XMLConstants.XHTML_NAMESPACE_URI,
                            "optgroup",
                            optGroupQName,
                            optGroupAttributes);
                        optgroupCount++;
                      } else {
                        handleItemCompact(
                            contentHandler, optionQName, xformsSelect1Control, isMultiple, item);
                      }
                    }

                    public void endItem(ContentHandler contentHandler) throws SAXException {}
                  });
            }
          }
          contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "select", selectQName);
        }
      }
    } else {
      // Read-only mode

      final String spanQName = XMLUtils.buildQName(xhtmlPrefix, "span");
      contentHandler.startElement(
          XMLConstants.XHTML_NAMESPACE_URI, "span", spanQName, containerAttributes);
      if (!handlerContext.isTemplate()) {
        final String value =
            (xformsSelect1Control == null || xformsSelect1Control.getValue(pipelineContext) == null)
                ? ""
                : xformsSelect1Control.getValue(pipelineContext);
        final StringBuilder sb = new StringBuilder();
        if (itemset != null) {
          int selectedFound = 0;
          for (final Item currentItem : itemset.toList()) {
            if (XFormsItemUtils.isSelected(isMultiple, value, currentItem.getValue())) {
              if (selectedFound > 0) sb.append(" - ");
              sb.append(currentItem.getLabel());
              selectedFound++;
            }
          }
        }

        if (sb.length() > 0) {
          final String result = sb.toString();
          contentHandler.characters(result.toCharArray(), 0, result.length());
        }
      }
      contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "span", spanQName);
    }
  }