private XFormsInstance checkInstanceToUpdate( PropertyContext propertyContext, XFormsModelSubmission.SubmissionParameters p) { XFormsInstance updatedInstance; final NodeInfo destinationNodeInfo = submission.evaluateTargetRef( propertyContext, p.xpathContext, submission.findReplaceInstanceNoTargetref(p.refInstance), 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( propertyContext, submission, XFormsSubmitErrorEvent.ErrorType.TARGET_ERROR, null)); } updatedInstance = submission.getContainingDocument().getInstanceForNode(destinationNodeInfo); if (updatedInstance == null || !updatedInstance.getInstanceRootElementInfo().isSameNodeInfo(destinationNodeInfo)) { // Only support replacing the root element of an instance // TODO: in the future, check on resolvedXXFormsReadonly to implement this restriction only // when using a readonly instance throw new XFormsSubmissionException( submission, "targetref attribute must point to an instance root element when using cached/shared instance replacement.", "processing targetref attribute", new XFormsSubmitErrorEvent( propertyContext, submission, XFormsSubmitErrorEvent.ErrorType.TARGET_ERROR, null)); } if (XFormsServer.logger.isDebugEnabled()) submission .getContainingDocument() .logDebug( "submission", "using instance from application shared instance cache", "instance", updatedInstance.getEffectiveId()); return updatedInstance; }
public void setInstance(XFormsInstance instance) { final Document instanceDocument = instance.getDocument(); this.resultingRequestBodyHash = instance.getRequestBodyHash(); this.resultingDocument = (instanceDocument != null) ? instanceDocument : instance.getDocumentInfo(); }
public void replace( PropertyContext propertyContext, 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. However, in the dynamic // case, I think that this should be a (currently non-specified by XForms) // xforms-binding-error. submission .getXBLContainer(containingDocument) .dispatchEvent( propertyContext, new XFormsBindingExceptionEvent(containingDocument, submission)); } else { final NodeInfo destinationNodeInfo = submission.evaluateTargetRef( propertyContext, 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, propertyContext, 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, propertyContext, submission, XFormsSubmitErrorEvent.ErrorType.TARGET_ERROR, connectionResult)); } // Whether the destination node is the root element of an instance final boolean isDestinationRootElement = updatedInstance.getInstanceRootElementInfo().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, propertyContext, submission, XFormsSubmitErrorEvent.ErrorType.TARGET_ERROR, connectionResult)); } final IndentedLogger detailsLogger = getDetailsLogger(p, p2); // Obtain root element to insert final NodeInfo newDocumentRootElement; final XFormsInstance newInstance; { // Create resulting instance whether entire instance is replaced or not, because this: // 1. Wraps a Document within a DocumentInfo if needed // 2. Performs text nodes adjustments if needed if (!p2.isReadonly) { // Resulting instance must not be read-only if (detailsLogger.isDebugEnabled()) detailsLogger.logDebug( "", "replacing instance with mutable instance", "instance", updatedInstance.getEffectiveId()); newInstance = new XFormsInstance( updatedInstance.getEffectiveModelId(), updatedInstance.getId(), (Document) resultingDocument, connectionResult.resourceURI, resultingRequestBodyHash, p2.username, p2.password, p2.isCache, p2.timeToLive, updatedInstance.getValidation(), p2.isHandleXInclude, XFormsProperties.isExposeXPathTypes(containingDocument)); } else { // Resulting instance must be read-only if (detailsLogger.isDebugEnabled()) detailsLogger.logDebug( "", "replacing instance with read-only instance", "instance", updatedInstance.getEffectiveId()); newInstance = new ReadonlyXFormsInstance( updatedInstance.getEffectiveModelId(), updatedInstance.getId(), (DocumentInfo) resultingDocument, connectionResult.resourceURI, resultingRequestBodyHash, p2.username, p2.password, p2.isCache, p2.timeToLive, updatedInstance.getValidation(), p2.isHandleXInclude, XFormsProperties.isExposeXPathTypes(containingDocument)); } newDocumentRootElement = newInstance.getInstanceRootElementInfo(); } // 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" if (isDestinationRootElement) { // Optimized insertion for instance root element replacement // Handle new instance and associated event markings final XFormsModel replaceModel = newInstance.getModel(containingDocument); replaceModel.handleUpdatedInstance(propertyContext, newInstance, newDocumentRootElement); // 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 newInstance .getXBLContainer(containingDocument) .dispatchEvent( propertyContext, new XFormsInsertEvent( containingDocument, newInstance, Collections.singletonList((Item) newDocumentRootElement), null, newDocumentRootElement.getDocumentRoot(), "after", null, null, true)); } else { // Generic insertion 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 final List insertedNode = XFormsInsertAction.doInsert( propertyContext, 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( propertyContext, containingDocument, detailsLogger, destinationCollection, 1, true); } // Perform model instance update // Handle new instance and associated event markings // 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? final XFormsModel updatedModel = updatedInstance.getModel(containingDocument); updatedModel.handleUpdatedInstance( propertyContext, updatedInstance, (NodeInfo) insertedNode.get(0)); } // Dispatch xforms-submit-done dispatchSubmitDone(propertyContext, connectionResult); } }
public SubmissionResult connect( final PropertyContext propertyContext, final XFormsModelSubmission.SubmissionParameters p, final XFormsModelSubmission.SecondPassParameters p2, final XFormsModelSubmission.SerializationParameters sp) throws Exception { // Get the instance from shared instance cache // This can only happen is method="get" and replace="instance" and xxforms:cache="true" or // xxforms:shared="application" // Convert URL to string final String absoluteResolvedURLString; { final ExternalContext externalContext = getExternalContext(propertyContext); final URL absoluteResolvedURL = getResolvedSubmissionURL( propertyContext, externalContext, p2.actionOrResource, sp.queryString); absoluteResolvedURLString = absoluteResolvedURL.toExternalForm(); } // Compute a hash of the body if needed final String requestBodyHash; if (sp.messageBody != null) { requestBodyHash = SecureUtils.digestBytes(sp.messageBody, "MD5", "hex"); } else { requestBodyHash = null; } // Parameters to callable final String submissionEffectiveId = submission.getEffectiveId(); final String instanceStaticId; final String modelEffectiveId; final String validation; { // Find and check replacement location final XFormsInstance updatedInstance = checkInstanceToUpdate(propertyContext, p); instanceStaticId = updatedInstance.getId(); modelEffectiveId = updatedInstance.getEffectiveModelId(); validation = updatedInstance.getValidation(); } final boolean isReadonly = p2.isReadonly; final boolean handleXInclude = p2.isHandleXInclude; final long timeToLive = p2.timeToLive; // Create new logger as the submission might be asynchronous final IndentedLogger submissionLogger = new IndentedLogger(containingDocument.getIndentedLogger()); // Obtain replacer // Pass a pseudo connection result which contains information used by getReplacer() // We know that we will get an InstanceReplacer final ConnectionResult connectionResult = createPseudoConnectionResult(absoluteResolvedURLString); final InstanceReplacer replacer = (InstanceReplacer) submission.getReplacer(propertyContext, connectionResult, p); // Try from cache first final XFormsInstance cacheResult = XFormsServerSharedInstancesCache.instance() .findConvertNoLoad( propertyContext, submissionLogger, instanceStaticId, modelEffectiveId, absoluteResolvedURLString, requestBodyHash, isReadonly, handleXInclude, XFormsProperties.isExposeXPathTypes(containingDocument)); if (cacheResult != null) { // Result was immediately available, so return it right away // The purpose of this is to avoid starting a new thread in asynchronous mode if the instance // is already in cache // Here we cheat a bit: instead of calling generically deserialize(), we directly set the // instance document replacer.setInstance(cacheResult); // Return result return new SubmissionResult(submissionEffectiveId, replacer, connectionResult); } else { // Create callable for synchronous or asynchronous loading final Callable<SubmissionResult> callable = new Callable<SubmissionResult>() { public SubmissionResult call() { try { final XFormsInstance newInstance = XFormsServerSharedInstancesCache.instance() .findConvert( propertyContext, submissionLogger, instanceStaticId, modelEffectiveId, absoluteResolvedURLString, requestBodyHash, isReadonly, handleXInclude, XFormsProperties.isExposeXPathTypes(containingDocument), timeToLive, validation, new XFormsServerSharedInstancesCache.Loader() { public ReadonlyXFormsInstance load( PropertyContext propertyContext, String instanceStaticId, String modelEffectiveId, String instanceSourceURI, boolean handleXInclude, long timeToLive, String validation) { // Call regular submission SubmissionResult submissionResult = null; try { // Run regular submission but force synchronous execution and // readonly result final XFormsModelSubmission.SecondPassParameters updatedP2 = p2.amend(false, true); submissionResult = new RegularSubmission(submission) .connect(propertyContext, p, updatedP2, sp); // Check if the connection returned a throwable final Throwable throwable = submissionResult.getThrowable(); if (throwable != null) { // Propagate throw new ThrowableWrapper( throwable, submissionResult.getConnectionResult()); } else { // There was no throwable // We know that RegularSubmission returns a Replacer with an // instance document final DocumentInfo documentInfo = (DocumentInfo) ((InstanceReplacer) submissionResult.getReplacer()) .getResultingDocument(); // Create new shared instance return new ReadonlyXFormsInstance( modelEffectiveId, instanceStaticId, documentInfo, instanceSourceURI, updatedP2.username, updatedP2.password, true, timeToLive, validation, handleXInclude, XFormsProperties.isExposeXPathTypes(containingDocument)); } } catch (ThrowableWrapper throwableWrapper) { // In case we just threw it above, just propagate throw throwableWrapper; } catch (Throwable throwable) { // Exceptions are handled further down throw new ThrowableWrapper( throwable, (submissionResult != null) ? submissionResult.getConnectionResult() : null); } } }); // Here we cheat a bit: instead of calling generically deserialize(), we directly // set the DocumentInfo replacer.setInstance(newInstance); // Return result return new SubmissionResult(submissionEffectiveId, replacer, connectionResult); } catch (ThrowableWrapper throwableWrapper) { // The ThrowableWrapper was thrown within the inner load() method above return new SubmissionResult( submissionEffectiveId, throwableWrapper.getThrowable(), throwableWrapper.getConnectionResult()); } catch (Throwable throwable) { // Any other throwable return new SubmissionResult(submissionEffectiveId, throwable, null); } } }; // Submit the callable // This returns null if the execution is asynchronous return submitCallable(p, p2, callable); } }
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); } }
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()); } }
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; }