コード例 #1
0
  @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);
    }
  }
コード例 #2
0
 private String findDefaultSelectedCaseId() {
   // TODO: Use ElementAnalysis instead
   final List<Element> caseElements =
       Dom4jUtils.elements(element(), XFormsConstants.XFORMS_CASE_QNAME);
   for (final Element caseElement : caseElements) {
     if (XFormsCaseControl.isDefaultSelected(caseElement)) {
       // Found first case with selected="true"
       return XFormsUtils.getElementId(caseElement);
     }
   }
   // Didn't find a case with selected="true" so return first case
   return XFormsUtils.getElementId(caseElements.get(0));
 }
コード例 #3
0
  // Test that xxf:attribute elements with @id and @for were created for
  @Test
  public void xxformsAttribute() {

    final Document document =
        Dom4jUtils.readFromURL(
            "oxf:/org/orbeon/oxf/xforms/processor/test-form.xml",
            XMLUtils.ParserConfiguration.PLAIN);
    final Metadata metadata = new Metadata();
    final Document annotatedDocument =
        new XBLBindings(new IndentedLogger(XFormsServer.logger, ""), null, metadata)
            .annotateShadowTree(document, "", false);
    final DocumentWrapper documentWrapper =
        new DocumentWrapper(annotatedDocument, null, XPathCache.getGlobalConfiguration());

    // Check there is an xxf:attribute for "html" with correct name
    List<Object> result =
        XPathCache.evaluate(
            documentWrapper,
            "//xxf:attribute[@for = 'html']",
            XFormsStaticStateImpl.BASIC_NAMESPACE_MAPPING(),
            null,
            null,
            null,
            null,
            null);

    assertNotNull(result);
    assertEquals(1, result.size());
    Element resultElement = (Element) ((NodeWrapper) result.get(0)).getUnderlyingNode();
    assertTrue(XFormsUtils.getElementId(resultElement).trim().length() > 0);
    assertEquals("lang", resultElement.attributeValue(XFormsConstants.NAME_QNAME));

    // Check there is an xxf:attribute for "span" with correct name
    result =
        XPathCache.evaluate(
            documentWrapper,
            "//xxf:attribute[@for = 'span']",
            XFormsStaticStateImpl.BASIC_NAMESPACE_MAPPING(),
            null,
            null,
            null,
            null,
            null);

    assertNotNull(result);
    assertEquals(1, result.size());
    resultElement = (Element) ((NodeWrapper) result.get(0)).getUnderlyingNode();
    assertTrue(XFormsUtils.getElementId(resultElement).trim().length() > 0);
    assertEquals("style", resultElement.attributeValue(XFormsConstants.NAME_QNAME));
  }
コード例 #4
0
  @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();
    }
  }
コード例 #5
0
 private String getOtherSelectedCaseEffectiveId(XFormsSwitchControl switchControl1) {
   if (switchControl1 != null && switchControl1.isRelevant()) {
     final String selectedCaseId =
         ((XFormsSwitchControlLocal) switchControl1.getInitialLocal()).selectedCaseControlId;
     return XFormsUtils.getRelatedEffectiveId(switchControl1.getEffectiveId(), selectedCaseId);
   } else {
     return null;
   }
 }
コード例 #6
0
 /**
  * Get the effective id of the currently selected case.
  *
  * @return effective id
  */
 public String getSelectedCaseEffectiveId() {
   if (isRelevant()) {
     final XFormsSwitchControlLocal local = (XFormsSwitchControlLocal) getCurrentLocal();
     if (local.selectedCaseControlId != null) {
       return XFormsUtils.getRelatedEffectiveId(getEffectiveId(), local.selectedCaseControlId);
     } else {
       throw new OXFException("Selected case was not set for xf:switch: " + getEffectiveId());
     }
   } else {
     return null;
   }
 }
コード例 #7
0
  public void execute(
      XFormsActionInterpreter actionInterpreter,
      PropertyContext propertyContext,
      String targetId,
      XFormsEventObserver eventObserver,
      Element actionElement,
      XBLBindings.Scope actionScope,
      boolean hasOverriddenContext,
      Item overriddenContext) {

    final XFormsContainingDocument containingDocument = actionInterpreter.getContainingDocument();

    final String controlIdAttributeValue =
        XFormsUtils.namespaceId(containingDocument, actionElement.attributeValue("control"));
    if (controlIdAttributeValue == null)
      throw new OXFException("Missing mandatory 'control' attribute on xforms:control element.");

    final String resolvedControlStaticId;
    {
      // Resolve AVT
      resolvedControlStaticId =
          actionInterpreter.resolveAVTProvideValue(
              propertyContext, actionElement, controlIdAttributeValue, true);
      if (resolvedControlStaticId == null) return;
    }

    final Object controlObject =
        actionInterpreter.resolveEffectiveControl(
            propertyContext, actionElement, resolvedControlStaticId);
    if (controlObject instanceof XFormsControl) {
      // Dispatch event to control object
      containingDocument.dispatchEvent(
          propertyContext,
          new XFormsFocusEvent(containingDocument, (XFormsEventTarget) controlObject));
    } else {
      // "If there is a null search result for the target object and the source object is an XForms
      // action such as
      // dispatch, send, setfocus, setindex or toggle, then the action is terminated with no
      // effect."
      final IndentedLogger indentedLogger = actionInterpreter.getIndentedLogger();
      if (indentedLogger.isDebugEnabled())
        indentedLogger.logDebug(
            "xforms:setfocus",
            "control does not refer to an existing control element, ignoring action",
            "control id",
            resolvedControlStaticId);
    }
  }
コード例 #8
0
  @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!
  }
コード例 #9
0
  @Override
  public void outputAjaxDiff(
      ContentHandlerHelper ch,
      XFormsControl other,
      AttributesImpl attributesImpl,
      boolean isNewlyVisibleSubtree) {
    // Output regular diff
    super.outputAjaxDiff(ch, other, attributesImpl, isNewlyVisibleSubtree);

    // Output switch-specific diff if needed only
    final XFormsSwitchControl otherSwitchControl = (XFormsSwitchControl) other;
    if (!compareSelectedCase(otherSwitchControl)) {

      if (isRelevant()) {

        // Output newly selected case id
        final String selectedCaseEffectiveId = getSelectedCaseEffectiveId();
        assert selectedCaseEffectiveId != null;
        ch.element(
            "xxf",
            XFormsConstants.XXFORMS_NAMESPACE_URI,
            "div",
            new String[] {
              "id",
              XFormsUtils.namespaceId(containingDocument(), selectedCaseEffectiveId),
              "visibility",
              "visible"
            });

        if (otherSwitchControl != null && otherSwitchControl.isRelevant()) {
          // Used to be relevant, simply output deselected case ids
          final String previousSelectedCaseId = getOtherSelectedCaseEffectiveId(otherSwitchControl);
          assert previousSelectedCaseId != null;
          ch.element(
              "xxf",
              XFormsConstants.XXFORMS_NAMESPACE_URI,
              "div",
              new String[] {
                "id",
                XFormsUtils.namespaceId(containingDocument(), previousSelectedCaseId),
                "visibility",
                "hidden"
              });
        } else {
          // Control was not relevant, send all deselected to be sure
          // TODO: This should not be needed because the repeat template should have a reasonable
          // default.
          final List<XFormsCaseControl> children = getChildrenCases();
          if (children != null && children.size() > 0) {
            for (final XFormsCaseControl caseControl : children) {
              if (!caseControl.getEffectiveId().equals(selectedCaseEffectiveId)) {
                ch.element(
                    "xxf",
                    XFormsConstants.XXFORMS_NAMESPACE_URI,
                    "div",
                    new String[] {
                      "id",
                      XFormsUtils.namespaceId(containingDocument(), caseControl.getEffectiveId()),
                      "visibility",
                      "hidden"
                    });
              }
            }
          }
        }
      }
    }
  }
コード例 #10
0
 private boolean compareSelectedCase(XFormsSwitchControl otherSwitchControl) {
   return XFormsUtils.compareStrings(
       getSelectedCaseEffectiveId(), getOtherSelectedCaseEffectiveId(otherSwitchControl));
 }
コード例 #11
0
 @Override
 public Map<String, String> serializeLocal() {
   // Serialize case id
   return Collections.singletonMap(
       "case-id", XFormsUtils.getStaticIdFromId(getSelectedCaseEffectiveId()));
 }
コード例 #12
0
  private void gatherInputDependencies(
      PipelineContext pipelineContext,
      XFormsContainingDocument containingDocument,
      IndentedLogger indentedLogger,
      Stage1CacheableState stage1CacheableState) {

    final String forwardSubmissionHeaders =
        XFormsProperties.getForwardSubmissionHeaders(containingDocument);

    // Add static instance source dependencies for top-level models
    // TODO: check all models/instances
    final XFormsStaticState staticState = containingDocument.getStaticState();
    for (final Model model :
        staticState.getModelsForScope(staticState.getXBLBindings().getTopLevelScope())) {
      for (final Instance instance : model.instancesMap().values()) {
        if (instance.dependencyURL() != null) {

          final String resolvedDependencyURL =
              XFormsUtils.resolveServiceURL(
                  pipelineContext,
                  containingDocument,
                  instance.element(),
                  instance.dependencyURL(),
                  ExternalContext.Response.REWRITE_MODE_ABSOLUTE);

          if (!instance.isCacheHint()) {
            stage1CacheableState.addReference(
                null,
                resolvedDependencyURL,
                instance.xxformsUsername(),
                instance.xxformsPassword(),
                instance.xxformsPassword(),
                forwardSubmissionHeaders);

            if (indentedLogger.isDebugEnabled())
              indentedLogger.logDebug(
                  "",
                  "adding document cache dependency for non-cacheable instance",
                  "instance URI",
                  resolvedDependencyURL);

          } else {
            // Don't add the dependency as we don't want the instance URI to be hit
            // For all practical purposes, globally shared instances must remain constant!
            if (indentedLogger.isDebugEnabled())
              indentedLogger.logDebug(
                  "",
                  "not adding document cache dependency for cacheable instance",
                  "instance URI",
                  resolvedDependencyURL);
          }
        }
      }
    }

    // Set caching dependencies if the input was actually read
    // TODO: check all models/instances
    // Q: should use static dependency information instead? what about schema imports and instance
    // replacements?
    for (final XFormsModel currentModel : containingDocument.getModels()) {
      // Add schema dependencies
      final String[] schemaURIs = currentModel.getSchemaURIs();
      // TODO: We should also use dependencies computed in XFormsModelSchemaValidator.SchemaInfo
      if (schemaURIs != null) {
        for (final String currentSchemaURI : schemaURIs) {
          if (indentedLogger.isDebugEnabled())
            indentedLogger.logDebug(
                "", "adding document cache dependency for schema", "schema URI", currentSchemaURI);

          stage1CacheableState.addReference(
              null,
              currentSchemaURI,
              null,
              null,
              null,
              forwardSubmissionHeaders); // TODO: support username / password on schema refs
        }
      }
    }
    // TODO: Add @src attributes from controls? Not used often.

    // Set caching dependencies for XBL inclusions
    {
      final XFormsStaticState.Metadata metadata = containingDocument.getStaticState().getMetadata();
      final Set<String> includes = metadata.getBindingsIncludes();
      if (includes != null) {
        for (final String include : includes) {
          stage1CacheableState.addReference(null, "oxf:" + include, null, null, null, null);
        }
      }
    }
  }
コード例 #13
0
  private void doIt(
      final PipelineContext pipelineContext,
      XMLReceiver xmlReceiver,
      final URIProcessorOutputImpl processorOutput,
      String outputName) {

    final ExternalContext externalContext = XFormsUtils.getExternalContext(pipelineContext);
    final IndentedLogger indentedLogger =
        XFormsContainingDocument.getIndentedLogger(
            XFormsToXHTML.logger, XFormsServer.getLogger(), LOGGING_CATEGORY);

    // ContainingDocument and XFormsState created below
    final XFormsContainingDocument[] containingDocument = new XFormsContainingDocument[1];
    final boolean[] cachedStatus = new boolean[] {false};

    final Stage2CacheableState stage2CacheableState;
    if (TEST_STATE == null) {

      // Read and try to cache the complete XForms+XHTML document with annotations
      stage2CacheableState =
          (Stage2CacheableState)
              readCacheInputAsObject(
                  pipelineContext,
                  getInputByName(INPUT_ANNOTATED_DOCUMENT),
                  new CacheableInputReader() {
                    public Object read(
                        PipelineContext pipelineContext, ProcessorInput processorInput) {

                      // Compute annotated XForms document + static state document
                      final Stage1CacheableState stage1CacheableState = new Stage1CacheableState();
                      final Stage2CacheableState stage2CacheableState;
                      final XFormsStaticState[] staticState = new XFormsStaticState[1];
                      {
                        // Store dependencies container in state before reading
                        ((Stage2TransientState) XFormsToXHTML.this.getState(pipelineContext))
                                .stage1CacheableState =
                            stage1CacheableState;

                        // Read static state from input
                        stage2CacheableState =
                            readStaticState(
                                pipelineContext, externalContext, indentedLogger, staticState);
                      }

                      // Create containing document and initialize XForms engine
                      // NOTE: Create document here so we can do appropriate analysis of caching
                      // dependencies
                      final XFormsURIResolver uriResolver =
                          new XFormsURIResolver(
                              XFormsToXHTML.this,
                              processorOutput,
                              pipelineContext,
                              INPUT_ANNOTATED_DOCUMENT,
                              URLGenerator.DEFAULT_HANDLE_XINCLUDE);
                      containingDocument[0] =
                          new XFormsContainingDocument(
                              pipelineContext,
                              staticState[0],
                              stage2CacheableState.getAnnotatedTemplate(),
                              uriResolver);

                      // Gather set caching dependencies
                      gatherInputDependencies(
                          pipelineContext,
                          containingDocument[0],
                          indentedLogger,
                          stage1CacheableState);

                      return stage2CacheableState;
                    }

                    @Override
                    public void foundInCache() {
                      cachedStatus[0] = true;
                    }
                  },
                  false);

      TEST_STATE = DO_TEST_STATE ? stage2CacheableState : null;

    } else {
      stage2CacheableState = TEST_STATE;
    }

    try {
      // Create containing document if not done yet
      if (containingDocument[0] == null) {
        assert cachedStatus[0];
        // In this case, we found the static state digest and more in the cache, but we must now
        // create a new XFormsContainingDocument from this information
        indentedLogger.logDebug(
            "",
            "annotated document and static state digest obtained from cache",
            "digest",
            stage2CacheableState.getStaticStateDigest());

        final XFormsStaticState staticState;
        {
          final XFormsStaticState cachedState =
              XFormsStaticStateCache.instance()
                  .getDocument(pipelineContext, stage2CacheableState.getStaticStateDigest());
          if (cachedState != null && cachedState.getMetadata().checkBindingsIncludes()) {
            // Found static state in cache
            indentedLogger.logDebug("", "found up-to-date static state by digest in cache");

            staticState = cachedState;
          } else {
            // Not found static state in cache OR it is out of date, create static state from input
            // NOTE: In out of date case, could clone static state and reprocess instead?
            if (cachedState != null)
              indentedLogger.logDebug("", "found out-of-date static state by digest in cache");
            else indentedLogger.logDebug("", "did not find static state by digest in cache");

            final StaticStateBits staticStateBits =
                new StaticStateBits(
                    pipelineContext,
                    externalContext,
                    indentedLogger,
                    stage2CacheableState.getStaticStateDigest());
            staticState =
                new XFormsStaticState(
                    pipelineContext,
                    staticStateBits.staticStateDocument,
                    stage2CacheableState.getStaticStateDigest(),
                    staticStateBits.metadata);

            // Store in cache
            XFormsStaticStateCache.instance().storeDocument(pipelineContext, staticState);
          }
        }

        final XFormsURIResolver uriResolver =
            new XFormsURIResolver(
                XFormsToXHTML.this,
                processorOutput,
                pipelineContext,
                INPUT_ANNOTATED_DOCUMENT,
                URLGenerator.DEFAULT_HANDLE_XINCLUDE);
        containingDocument[0] =
            new XFormsContainingDocument(
                pipelineContext,
                staticState,
                stage2CacheableState.getAnnotatedTemplate(),
                uriResolver);
      } else {
        assert !cachedStatus[0];
        indentedLogger.logDebug(
            "", "annotated document and static state digest not obtained from cache.");
      }

      // Output resulting document
      if (outputName.equals("document")) {
        // Normal case where we output XHTML
        outputResponseDocument(
            pipelineContext,
            externalContext,
            indentedLogger,
            stage2CacheableState.getAnnotatedTemplate(),
            containingDocument[0],
            xmlReceiver);
      } else {
        // Output in test mode
        testOutputResponseState(
            pipelineContext, containingDocument[0], indentedLogger, xmlReceiver);
      }

      // Notify state manager
      XFormsStateManager.instance().afterInitialResponse(pipelineContext, containingDocument[0]);

    } catch (Throwable e) {
      indentedLogger.logDebug("", "throwable caught during initialization.");
      throw new OXFException(e);
    }
  }
コード例 #14
0
 public static String getItemId(String effectiveId, String itemIndex) {
   return XFormsUtils.appendToEffectiveId(effectiveId, "$$e" + itemIndex);
 }
コード例 #15
0
  public static List<NodeInfo> doInsert(
      XFormsContainingDocument containingDocument,
      IndentedLogger indentedLogger,
      String positionAttribute,
      List collectionToBeUpdated,
      NodeInfo insertContextNodeInfo,
      List<Item> originItems,
      int insertionIndex,
      boolean doClone,
      boolean doDispatch,
      boolean requireDefaultValues) {

    final boolean isEmptyNodesetBinding =
        collectionToBeUpdated == null || collectionToBeUpdated.size() == 0;

    final NodeInfo insertLocationNodeInfo;
    if (isEmptyNodesetBinding) {
      // Insert INTO a node

      // "If the Node Set Binding node-set is not specified or empty, the insert location node is
      // the insert
      // context node."

      // "a. If the Node Set Binding node-set is not specified or empty, the target location depends
      // on the
      // node type of the cloned node. If the cloned node is an attribute, then the target location
      // is before
      // the first attribute of the insert location node. If the cloned node is not an attribute,
      // then the
      // target location is before the first child of the insert location node."

      insertLocationNodeInfo = insertContextNodeInfo;
    } else {
      // Insert BEFORE or AFTER a node
      insertLocationNodeInfo = (NodeInfo) collectionToBeUpdated.get(insertionIndex - 1);
    }

    // Identify the instance that actually changes
    final XFormsInstance modifiedInstanceOrNull =
        (containingDocument != null)
            ? containingDocument.getInstanceForNode(insertLocationNodeInfo)
            : null;

    // NOTE: The check on `hasAnyCalculationBind` is not optimal: we should check whether
    // specifically there are any xxf:default which can touch this
    // instance, ideally.
    // NOTE: We do this test here so that we don't unnecessarily annotate nodes.
    final boolean applyDefaults =
        requireDefaultValues
            && modifiedInstanceOrNull != null
            && modifiedInstanceOrNull.model().staticModel().hasDefaultValueBind()
            && containingDocument
                .getXPathDependencies()
                .hasAnyCalculationBind(
                    modifiedInstanceOrNull.model().staticModel(),
                    modifiedInstanceOrNull.getPrefixedId());

    // "3. The origin node-set is determined."
    // "5. Each node in the origin node-set is cloned in the order it appears in the origin
    // node-set."
    final List<Node> sourceNodes;
    final List<Node> clonedNodes;
    {
      final List<Node> clonedNodesTemp;
      if (originItems == null) {
        // There are no explicitly specified origin objects, use node from Node Set Binding node-set

        // "If the origin attribute is not given and the Node Set Binding node-set is empty, then
        // the origin
        // node-set is the empty node-set. [...] The insert action is terminated with no effect if
        // the
        // origin node-set is the empty node-set."

        if (isEmptyNodesetBinding) {
          if (indentedLogger != null && indentedLogger.isDebugEnabled())
            indentedLogger.logDebug(
                "xf:insert", "origin node-set from node-set binding is empty, terminating");
          return Collections.emptyList();
        }

        // "Otherwise, if the origin attribute is not given, then the origin node-set consists of
        // the last
        // node of the Node Set Binding node-set."
        final Node singleSourceNode =
            XFormsUtils.getNodeFromNodeInfoConvert(
                (NodeInfo) collectionToBeUpdated.get(collectionToBeUpdated.size() - 1));
        // TODO: check namespace handling might be incorrect. Should use
        // copyElementCopyParentNamespaces() instead?
        final Node singleClonedNode = Dom4jUtils.createCopy(singleSourceNode);

        sourceNodes = Collections.singletonList(singleSourceNode);
        clonedNodesTemp = Collections.singletonList(singleClonedNode);
      } else {
        // There are explicitly specified origin objects

        // "The insert action is terminated with no effect if the origin node-set is the empty
        // node-set."
        if (originItems.size() == 0) {
          if (indentedLogger != null && indentedLogger.isDebugEnabled())
            indentedLogger.logDebug("xf:insert", "origin node-set is empty, terminating");
          return Collections.emptyList();
        }

        // "Each node in the origin node-set is cloned in the order it appears in the origin
        // node-set."

        sourceNodes = new ArrayList<Node>(originItems.size()); // set to max possible size
        clonedNodesTemp = new ArrayList<Node>(originItems.size());

        for (final Object currentObject : originItems) {
          if (currentObject instanceof NodeInfo) {
            // This is the regular case covered by XForms 1.1 / XPath 1.0

            // NOTE: Don't clone nodes if doClone == false
            final Node sourceNode =
                XFormsUtils.getNodeFromNodeInfoConvert((NodeInfo) currentObject);
            final Node clonedNode =
                doClone
                    ? (sourceNode instanceof Element)
                        ? ((Element) sourceNode).createCopy()
                        : (Node) sourceNode.clone()
                    : sourceNode;

            sourceNodes.add(sourceNode);
            clonedNodesTemp.add(clonedNode);

          } else if (currentObject instanceof AtomicValue) {
            // This is an extension: support sequences containing atomic values

            // Convert the result to a text node
            final String stringValue = ((Item) currentObject).getStringValue();
            final Text textNode = DocumentFactory.createText(stringValue);

            sourceNodes.add(
                null); // there is no source node for this cloned node, it's a source item
            clonedNodesTemp.add(textNode);
          } else throw new IllegalStateException();
        }
      }

      // Remove instance data from cloned nodes and perform Document node adjustment
      for (int i = 0; i < clonedNodesTemp.size(); i++) {
        final Node clonedNodeTemp = clonedNodesTemp.get(i);

        if (clonedNodeTemp instanceof Element) {
          // Element node
          if (applyDefaults) InstanceDataOps.setRequireDefaultValueRecursively(clonedNodeTemp);
          else InstanceDataOps.removeRecursively(clonedNodeTemp);
          clonedNodeTemp.detach();
        } else if (clonedNodeTemp instanceof Attribute) {
          // Attribute node
          if (applyDefaults) InstanceDataOps.setRequireDefaultValueRecursively(clonedNodeTemp);
          else InstanceDataOps.removeRecursively(clonedNodeTemp);
          clonedNodeTemp.detach();
        } else if (clonedNodeTemp instanceof Document) {
          // Document node
          final Element clonedNodeTempRootElement = clonedNodeTemp.getDocument().getRootElement();

          if (clonedNodeTempRootElement == null) {
            // Can be null in rare cases of documents without root element
            clonedNodesTemp.set(
                i, null); // we support having a null node further below, so set this to null
          } else {
            if (applyDefaults)
              InstanceDataOps.setRequireDefaultValueRecursively(clonedNodeTempRootElement);
            else InstanceDataOps.removeRecursively(clonedNodeTempRootElement);
            // We can never really insert a document into anything at this point, but we assume that
            // this means the root element
            clonedNodesTemp.set(i, clonedNodeTempRootElement.detach());
          }
        } else {
          // Other nodes
          clonedNodeTemp.detach();
        }
      }
      clonedNodes = clonedNodesTemp;
    }

    // "6. The target location of each cloned node or nodes is determined"
    // "7. The cloned node or nodes are inserted in the order they were cloned at their target
    // location
    // depending on their node type."

    // Find actual insertion point and insert
    final int insertLocationIndexWithinParentBeforeUpdate;
    final List<Node> insertedNodes;
    final String beforeAfterInto;
    if (isEmptyNodesetBinding) {
      // Insert INTO a node

      insertLocationIndexWithinParentBeforeUpdate =
          findNodeIndexRewrapIfNeeded(insertLocationNodeInfo);

      final Node insertLocationNode =
          XFormsUtils.getNodeFromNodeInfo(insertContextNodeInfo, CANNOT_INSERT_READONLY_MESSAGE);
      insertedNodes = doInsert(insertLocationNode, clonedNodes, modifiedInstanceOrNull, doDispatch);
      beforeAfterInto = "into";

      // Normalize text nodes if needed to respect XPath 1.0 constraint
      {
        boolean hasTextNode = false;
        for (Node clonedNode : clonedNodes) {
          hasTextNode |= clonedNode != null && (clonedNode instanceof Text);
        }
        if (hasTextNode) Dom4jUtils.normalizeTextNodes(insertLocationNode);
      }
    } else {
      // Insert BEFORE or AFTER a node

      insertLocationIndexWithinParentBeforeUpdate =
          findNodeIndexRewrapIfNeeded(insertLocationNodeInfo);

      final Node insertLocationNode =
          XFormsUtils.getNodeFromNodeInfo(insertLocationNodeInfo, CANNOT_INSERT_READONLY_MESSAGE);
      final Document insertLocationNodeDocument = insertLocationNode.getDocument();
      if (insertLocationNodeDocument != null
          && insertLocationNodeDocument.getRootElement() == insertLocationNode) {

        // "c. if insert location node is the root element of an instance, then that instance root
        // element
        // location is the target location. If there is more than one cloned node to insert, only
        // the
        // first node that does not cause a conflict is considered."

        insertedNodes =
            doInsert(
                insertLocationNode.getDocument(), clonedNodes, modifiedInstanceOrNull, doDispatch);
        beforeAfterInto = positionAttribute; // TODO: ideally normalize to "into document node"?

        // NOTE: Don't need to normalize text nodes in this case, as no new text node is inserted
      } else {
        // "d. Otherwise, the target location is immediately before or after the insert location
        // node, based on the position attribute setting or its default."

        if (insertLocationNode instanceof Attribute) {
          // Special case for "next to an attribute"

          // NOTE: In XML, attributes are unordered. dom4j handles them as a list so has order, but
          // the XForms spec shouldn't rely on attribute order. We could try to keep the order, but
          // it
          // is harder as we have to deal with removing duplicate attributes and find a reasonable
          // insertion strategy.

          // TODO: Don't think we should even do this now in XForms 1.1
          insertedNodes =
              doInsert(
                  insertLocationNode.getParent(), clonedNodes, modifiedInstanceOrNull, doDispatch);

        } else {
          // Other node types
          final Element parentNode = insertLocationNode.getParent();
          final List<Node> siblingElements = parentNode.content();
          final int actualIndex = siblingElements.indexOf(insertLocationNode);

          // Prepare insertion of new element
          final int actualInsertionIndex;
          if ("before".equals(positionAttribute)) {
            actualInsertionIndex = actualIndex;
          } else {
            // "after"
            actualInsertionIndex = actualIndex + 1;
          }

          // "7. The cloned node or nodes are inserted in the order they were cloned at their target
          // location depending on their node type."

          boolean hasTextNode = false;
          int addIndex = 0;
          insertedNodes = new ArrayList<Node>(clonedNodes.size());
          for (Node clonedNode : clonedNodes) {

            if (clonedNode != null) { // NOTE: we allow passing some null nodes so we check on null
              if (!(clonedNode instanceof Attribute || clonedNode instanceof Namespace)) {
                // Element, text, comment, processing instruction node
                siblingElements.add(actualInsertionIndex + addIndex, clonedNode);
                insertedNodes.add(clonedNode);
                hasTextNode |= clonedNode instanceof Text;
                addIndex++;
              } else {
                // We never insert attributes or namespace nodes as siblings
                if (indentedLogger != null && indentedLogger.isDebugEnabled())
                  indentedLogger.logDebug(
                      "xf:insert",
                      "skipping insertion of node as sibling in element content",
                      "type",
                      Node$.MODULE$.nodeTypeName(clonedNode),
                      "node",
                      clonedNode instanceof Attribute
                          ? Dom4jUtils.attributeToDebugString((Attribute) clonedNode)
                          : clonedNode.toString());
              }
            }
          }

          // Normalize text nodes if needed to respect XPath 1.0 constraint
          if (hasTextNode) Dom4jUtils.normalizeTextNodes(parentNode);
        }

        beforeAfterInto = positionAttribute;
      }
    }

    // Whether some nodes were inserted
    final boolean didInsertNodes = insertedNodes != null && insertedNodes.size() > 0;

    // Log stuff
    if (indentedLogger != null && indentedLogger.isDebugEnabled()) {
      if (didInsertNodes)
        indentedLogger.logDebug(
            "xf:insert",
            "inserted nodes",
            "count",
            Integer.toString(insertedNodes.size()),
            "instance",
            (modifiedInstanceOrNull != null) ? modifiedInstanceOrNull.getEffectiveId() : null);
      else indentedLogger.logDebug("xf:insert", "no node inserted");
    }

    // "XForms Actions that change the tree structure of instance data result in setting all four
    // flags to true"
    if (didInsertNodes && modifiedInstanceOrNull != null) {
      // NOTE: Can be null if document into which delete is performed is not in an instance, e.g. in
      // a variable
      modifiedInstanceOrNull.markModified();
      modifiedInstanceOrNull
          .model()
          .markStructuralChange(
              scala.Option.<XFormsInstance>apply(modifiedInstanceOrNull),
              FlaggedDefaultsStrategy$.MODULE$);
    }

    // Gather list of modified nodes
    final List<NodeInfo> insertedNodeInfos;
    if (didInsertNodes && modifiedInstanceOrNull != null) {
      // Instance can be null if document into which delete is performed is not in an instance, e.g.
      // in a variable
      final DocumentWrapper documentWrapper =
          (DocumentWrapper) modifiedInstanceOrNull.documentInfo();
      insertedNodeInfos = new ArrayList<NodeInfo>(insertedNodes.size());
      for (Node insertedNode : insertedNodes)
        insertedNodeInfos.add(documentWrapper.wrap(insertedNode));
    } else {
      insertedNodeInfos = Collections.emptyList();
    }

    // "4. If the insert is successful, the event xforms-insert is dispatched."
    // XFormsInstance handles index and repeat items updates
    if (doDispatch && didInsertNodes && modifiedInstanceOrNull != null) {

      // Adjust insert location node and before/after/into in case the root element was replaced
      final NodeInfo adjustedInsertLocationNodeInfo;
      final String adjustedBeforeAfterInto;

      final NodeInfo parent =
          insertedNodeInfos.get(0).getNodeKind() == org.w3c.dom.Node.ELEMENT_NODE
              ? insertedNodeInfos.get(0).getParent()
              : null;
      if (parent != null && parent.equals(parent.getDocumentRoot())) {
        // Node was inserted under document node
        adjustedInsertLocationNodeInfo = parent.getDocumentRoot();
        adjustedBeforeAfterInto = "into";
      } else {
        adjustedInsertLocationNodeInfo = rewrapIfNeeded(insertLocationNodeInfo);
        adjustedBeforeAfterInto = beforeAfterInto;
      }

      Dispatch.dispatchEvent(
          new XFormsInsertEvent(
              modifiedInstanceOrNull,
              insertedNodeInfos,
              originItems,
              adjustedInsertLocationNodeInfo,
              adjustedBeforeAfterInto,
              insertLocationIndexWithinParentBeforeUpdate));
    }

    return insertedNodeInfos;
  }