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); } }
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; }