Ejemplo n.º 1
0
 // See https://github.com/orbeon/orbeon-forms/issues/2803
 private static NodeInfo rewrapIfNeeded(NodeInfo node) {
   if (node instanceof VirtualNode) {
     final DocumentWrapper doc = (DocumentWrapper) node.getDocumentRoot();
     final Object underlying = ((VirtualNode) node).getUnderlyingNode();
     if (doc != null) return doc.wrap((Node) underlying);
     else
       return DocumentWrapper.makeWrapper(
           (Node)
               underlying); // unclear whether NodeWrappers are created with a null doc, but if so
                            // rewrapping this way should be ok
   } else return node;
 }
Ejemplo n.º 2
0
  private static List<Node> doInsert(
      Node insertionNode,
      List<Node> clonedNodes,
      XFormsInstance modifiedInstance,
      boolean doDispatch) {
    final List<Node> insertedNodes = new ArrayList<Node>(clonedNodes.size());
    if (insertionNode instanceof Element) {
      // Insert inside an element
      final Element insertContextElement = (Element) insertionNode;

      int otherNodeIndex = 0;
      for (Node clonedNode : clonedNodes) {

        if (clonedNode != null) { // NOTE: we allow passing some null nodes so we check on null
          if (clonedNode instanceof Attribute) {
            // Add attribute to element

            // 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.
            final Attribute clonedAttribute = (Attribute) clonedNode;
            final Attribute existingAttribute =
                insertContextElement.attribute(clonedAttribute.getQName());

            if (existingAttribute != null) insertContextElement.remove(existingAttribute);

            insertContextElement.add(clonedAttribute);

            if (existingAttribute != null) {

              // Dispatch xxforms-replace event if required and possible
              // NOTE: For now, still dispatch xforms-insert for backward compatibility.
              if (doDispatch && modifiedInstance != null) {
                final DocumentWrapper documentWrapper =
                    (DocumentWrapper) modifiedInstance.documentInfo();

                Dispatch.dispatchEvent(
                    new XXFormsReplaceEvent(
                        modifiedInstance,
                        documentWrapper.wrap(existingAttribute),
                        documentWrapper.wrap(clonedAttribute)));
              }
            }

            insertedNodes.add(clonedAttribute);

          } else if (!(clonedNode instanceof Document)) {
            // Add other node to element
            insertContextElement.content().add(otherNodeIndex++, clonedNode);
            insertedNodes.add(clonedNode);
          } else {
            // "If a cloned node cannot be placed at the target location due to a node type
            // conflict, then the
            // insertion for that particular clone node is ignored."
          }
        }
      }
      return insertedNodes;
    } else if (insertionNode instanceof Document) {
      final Document insertContextDocument = (Document) insertionNode;

      // "If there is more than one cloned node to insert, only the first node that does not cause a
      // conflict is
      // considered."
      for (Node clonedNode : clonedNodes) {
        // Only an element can be inserted at the root of an instance
        if (clonedNode instanceof Element) {

          final Element formerRootElement = insertContextDocument.getRootElement();
          insertContextDocument.setRootElement((Element) clonedNode);

          // Dispatch xxforms-replace event if required and possible
          // NOTE: For now, still dispatch xforms-insert for backward compatibility.
          if (doDispatch && modifiedInstance != null) {
            final DocumentWrapper documentWrapper =
                (DocumentWrapper) modifiedInstance.documentInfo();

            Dispatch.dispatchEvent(
                new XXFormsReplaceEvent(
                    modifiedInstance,
                    documentWrapper.wrap(formerRootElement),
                    documentWrapper.wrap(insertContextDocument.getRootElement())));
          }

          insertedNodes.add(clonedNode);
          return insertedNodes;
        }
      }

      // NOTE: The spec does not allow inserting comments and PIs at the root of an instance
      // document at this
      // point.

      return insertedNodes;
    } else {
      throw new OXFException(
          "Unsupported insertion node type: " + insertionNode.getClass().getName());
    }
  }
Ejemplo n.º 3
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;
  }