Ejemplo n.º 1
0
  public Runnable replace(
      final ConnectionResult connectionResult,
      XFormsModelSubmission.SubmissionParameters p,
      XFormsModelSubmission.SecondPassParameters p2) {

    // Set new instance document to replace the one submitted

    final XFormsInstance replaceInstanceNoTargetref =
        submission.findReplaceInstanceNoTargetref(p.refInstance);
    if (replaceInstanceNoTargetref == null) {

      // Replacement instance or node was specified but not found
      //
      // Not sure what's the right thing to do with 1.1, but this could be done
      // as part of the model's static analysis if the instance value is not
      // obtained through AVT, and dynamically otherwise.
      //
      // Another option would be to dispatch, at runtime, an xxforms-binding-error event.
      // xforms-submit-error is
      // consistent with targetref, so might be better.

      throw new XFormsSubmissionException(
          submission,
          "instance attribute doesn't point to an existing instance for replace=\"instance\".",
          "processing instance attribute",
          new XFormsSubmitErrorEvent(
              containingDocument,
              submission,
              XFormsSubmitErrorEvent.ErrorType.TARGET_ERROR,
              connectionResult));
    } else {

      final NodeInfo destinationNodeInfo =
          submission.evaluateTargetRef(
              p.xpathContext, replaceInstanceNoTargetref, p.submissionElementContextItem);

      if (destinationNodeInfo == null) {
        // Throw target-error

        // XForms 1.1: "If the processing of the targetref attribute fails,
        // then submission processing ends after dispatching the event
        // xforms-submit-error with an error-type of target-error."
        throw new XFormsSubmissionException(
            submission,
            "targetref attribute doesn't point to an element for replace=\"instance\".",
            "processing targetref attribute",
            new XFormsSubmitErrorEvent(
                containingDocument,
                submission,
                XFormsSubmitErrorEvent.ErrorType.TARGET_ERROR,
                connectionResult));
      }

      // This is the instance which is effectively going to be updated
      final XFormsInstance updatedInstance =
          containingDocument.getInstanceForNode(destinationNodeInfo);
      if (updatedInstance == null) {
        throw new XFormsSubmissionException(
            submission,
            "targetref attribute doesn't point to an element in an existing instance for replace=\"instance\".",
            "processing targetref attribute",
            new XFormsSubmitErrorEvent(
                containingDocument,
                submission,
                XFormsSubmitErrorEvent.ErrorType.TARGET_ERROR,
                connectionResult));
      }

      // Whether the destination node is the root element of an instance
      final boolean isDestinationRootElement =
          updatedInstance.instanceRoot().isSameNodeInfo(destinationNodeInfo);
      if (p2.isReadonly && !isDestinationRootElement) {
        // Only support replacing the root element of an instance when using a shared instance
        throw new XFormsSubmissionException(
            submission,
            "targetref attribute must point to instance root element when using read-only instance replacement.",
            "processing targetref attribute",
            new XFormsSubmitErrorEvent(
                containingDocument,
                submission,
                XFormsSubmitErrorEvent.ErrorType.TARGET_ERROR,
                connectionResult));
      }

      final IndentedLogger detailsLogger = getDetailsLogger(p, p2);

      // Obtain root element to insert
      if (detailsLogger.isDebugEnabled())
        detailsLogger.logDebug(
            "",
            p2.isReadonly
                ? "replacing instance with read-only instance"
                : "replacing instance with mutable instance",
            "instance",
            updatedInstance.getEffectiveId());

      // Perform insert/delete. This will dispatch xforms-insert/xforms-delete events.
      // "the replacement is performed by an XForms action that performs some
      // combination of node insertion and deletion operations that are
      // performed by the insert action (10.3 The insert Element) and the
      // delete action"

      // NOTE: As of 2009-03-18 decision, XForms 1.1 specifies that deferred event handling flags
      // are set instead of
      // performing RRRR directly.
      final DocumentInfo newDocumentInfo =
          wrappedDocumentInfo != null
              ? wrappedDocumentInfo
              : XFormsInstance.createDocumentInfo(
                  resultingDocumentOrDocumentInfo, updatedInstance.instance().exposeXPathTypes());

      if (isDestinationRootElement) {
        // Optimized insertion for instance root element replacement

        // Update the instance (this also marks it as modified)
        updatedInstance.update(
            Option.<InstanceCaching>apply(instanceCaching), newDocumentInfo, p2.isReadonly);

        final NodeInfo newDocumentRootElement = updatedInstance.instanceRoot();

        // Call this directly, since we are not using insert/delete here
        updatedInstance.model().markStructuralChange(updatedInstance);

        // Dispatch xforms-delete event
        // NOTE: Do NOT dispatch so we are compatible with the regular root element replacement
        // (see below). In the future, we might want to dispatch this, especially if
        // XFormsInsertAction dispatches xforms-delete when removing the root element
        // updatedInstance.getXBLContainer(containingDocument).dispatchEvent(pipelineContext, new
        // XFormsDeleteEvent(updatedInstance, Collections.singletonList(destinationNodeInfo), 1));

        // Dispatch xforms-insert event
        // NOTE: use the root node as insert location as it seems to make more sense than pointing
        // to the earlier root element
        Dispatch.dispatchEvent(
            new XFormsInsertEvent(
                containingDocument,
                updatedInstance,
                Collections.singletonList((Item) newDocumentRootElement),
                null,
                newDocumentRootElement.getDocumentRoot(),
                "after"));

      } else {
        // Generic insertion

        updatedInstance.markModified();
        final NodeInfo newDocumentRootElement = DataModel.firstChildElement(newDocumentInfo);

        final List<NodeInfo> destinationCollection = Collections.singletonList(destinationNodeInfo);

        // Perform the insertion

        // Insert before the target node, so that the position of the inserted node
        // wrt its parent does not change after the target node is removed
        // This will also mark a structural change
        XFormsInsertAction.doInsert(
            containingDocument,
            detailsLogger,
            "before",
            destinationCollection,
            destinationNodeInfo.getParent(),
            Collections.singletonList(newDocumentRootElement),
            1,
            false,
            true);

        if (!destinationNodeInfo
            .getParent()
            .isSameNodeInfo(destinationNodeInfo.getDocumentRoot())) {
          // The node to replace is NOT a root element

          // Perform the deletion of the selected node
          XFormsDeleteAction.doDelete(
              containingDocument, detailsLogger, destinationCollection, 1, true);
        }

        // Update model instance
        // NOTE: The inserted node NodeWrapper.index might be out of date at this point because:
        // * doInsert() dispatches an event which might itself change the instance
        // * doDelete() does as well
        // Does this mean that we should check that the node is still where it should be?
      }

      // Dispatch xforms-submit-done
      return submission.sendSubmitDone(connectionResult);
    }
  }
Ejemplo n.º 2
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;
  }