public void doSave(Widget frmModel, JXPathContext jctx) throws BindingException {
    // (See comment in doLoad about type checking and throwing a meaningful exception.)
    Repeater repeater = (Repeater) selectWidget(frmModel, this.repeaterId);

    // Perform shortcut binding if the repeater is empty
    // and the deleteIfEmpty config option is selected.
    if (repeater.getSize() == 0 && this.deleteIfEmpty) {
      // Delete all of the old data for this repeater.
      jctx.removeAll(this.repeaterPath);

      // Otherwise perform the normal save binding.
    } else {

      // Narrow to the repeater context, creating the path if it did not exist.
      JXPathContext repeaterContext = jctx.getRelativeContext(jctx.createPath(this.repeaterPath));

      // Start by deleting all of the old row data.
      repeaterContext.removeAll(this.rowPath);

      // Verify that repeater is not empty and has an insert row binding.
      if (repeater.getSize() > 0) {
        if (this.insertRowBinding != null) {

          // register the factory!
          // this.insertRowBinding.saveFormToModel(repeater, repeaterContext);

          // Iterate through the repeater rows.
          for (int i = 0; i < repeater.getSize(); i++) {

            // Narrow to the repeater row context.
            Pointer rowPointer = repeaterContext.getPointer(this.rowPathInsert);
            JXPathContext rowContext = repeaterContext.getRelativeContext(rowPointer);

            // Variables used for virtual rows.
            // They are initialized here just to keep the compiler happy.
            Node rowNode = null;
            Node virtualNode = null;

            // If virtual rows are requested, create a temporary node and
            // narrow the context to this initially empty new virtual row.
            if (virtualRows) {
              rowNode = (Node) rowContext.getContextBean();
              Document document = rowNode.getOwnerDocument();
              virtualNode = document.createElementNS(null, "virtual");
              Node fakeDocElement = document.getDocumentElement().cloneNode(false);
              fakeDocElement.appendChild(virtualNode);
              rowContext = JXPathContext.newContext(repeaterContext, fakeDocElement);
              rowContext = rowContext.getRelativeContext(rowContext.getPointer("virtual"));
            }

            // Perform the insert row binding
            this.insertRowBinding.saveFormToModel(repeater, rowContext);

            // Perform the save row binding.
            this.rowBinding.saveFormToModel(repeater.getRow(i), rowContext);

            // If virtual rows are requested, finish by appending the
            // children of the virtual row to the real context node.
            if (virtualRows) {
              NodeList list = virtualNode.getChildNodes();
              int count = list.getLength();
              for (int j = 0; j < count; j++) {
                // The list shrinks when a child is appended to the context
                // node, so we always reference the first child in the list.
                rowNode.appendChild(list.item(0));
              }
            }
            getLogger().debug("bound new row");
          }
        } else {
          getLogger()
              .warn(
                  "TempRepeaterBinding has detected rows to insert, "
                      + "but misses the <on-insert-row> binding to do it.");
        }
      }
    }
  }
  public void doLoad(Widget frmModel, JXPathContext jctx) throws BindingException {
    // (There should be a general widget type checker for all the bindings to use,
    // coupled with a general informative exception class to throw if the widget is
    // of the wrong type or null.)
    Repeater repeater = (Repeater) selectWidget(frmModel, this.repeaterId);
    if (repeater == null) {
      String fullId = frmModel.getRequestParameterName();
      if (fullId == null || fullId.length() == 0) {
        fullId = "";
      } else {
        fullId = fullId + ".";
      }
      throw new RuntimeException(
          "TempRepeaterJXPathBinding: Repeater \""
              + fullId
              + this.repeaterId
              + "\" does not exist ("
              + frmModel.getLocation()
              + ")");
    }

    // Start by clearing the repeater, if necessary.
    if (this.clearOnLoad) {
      repeater.clear();
    }

    // Find the location of the repeater data.
    Pointer repeaterPointer = jctx.getPointer(this.repeaterPath);

    // Check if there is data present.
    //
    // (Otherwise, should we check the leniency config option
    // to decide whether to be silent or throw an exception?)
    if (repeaterPointer != null) {

      // Narrow to repeater context.
      JXPathContext repeaterContext = jctx.getRelativeContext(repeaterPointer);

      // Build a jxpath iterator for the repeater row pointers.
      Iterator rowPointers = repeaterContext.iteratePointers(this.rowPath);

      // Iterate through the rows of data.
      int rowNum = 0;
      while (rowPointers.hasNext()) {

        // Get or create a row widget.
        Repeater.RepeaterRow thisRow;
        if (repeater.getSize() > rowNum) {
          thisRow = repeater.getRow(rowNum);
        } else {
          thisRow = repeater.addRow();
        }
        rowNum++;

        // Narrow to the row context.
        Pointer rowPointer = (Pointer) rowPointers.next();
        JXPathContext rowContext = repeaterContext.getRelativeContext(rowPointer);

        // If virtual rows are requested, place a deep clone of the row data
        // into a temporary node, and narrow the context to this virtual row.
        //
        // (A clone of the data is used to prevent modifying the source document.
        // Otherwise, the appendChild method would remove the data from the source
        // document.  Is this protection worth the penalty of a deep clone?)
        //
        // (This implementation of virtual rows currently only supports DOM
        // bindings, but could easily be extended to support other bindings.)

        if (virtualRows) {
          Node repeaterNode = (Node) repeaterPointer.getNode();
          Node virtualNode = repeaterNode.getOwnerDocument().createElementNS(null, "virtual");
          Node node = (Node) rowPointer.getNode();
          Node clone = node.cloneNode(true);
          Node fakeDocElement = node.getOwnerDocument().getDocumentElement().cloneNode(false);
          virtualNode.appendChild(clone);
          fakeDocElement.appendChild(virtualNode);
          rowContext = JXPathContext.newContext(repeaterContext, fakeDocElement);
          rowContext = rowContext.getRelativeContext(rowContext.getPointer("virtual"));
        }

        // Finally, perform the load row binding.
        this.rowBinding.loadFormFromModel(thisRow, rowContext);
      }
    }

    if (getLogger().isDebugEnabled()) getLogger().debug("done loading rows " + this);
  }