@Override protected void handleControlEnd( String uri, String localname, String qName, Attributes attributes, String staticId, String effectiveId, XFormsControl control) throws SAXException { handlerContext.popCaseContext(); final ElementHandlerController controller = handlerContext.getController(); if (!handlerContext.isNoScript()) { currentOutputInterceptor.flushCharacters(true, true); // Restore output controller.setOutput(currentSavedOutput); final boolean isMustGenerateBeginEndDelimiters = !handlerContext.isFullUpdateTopLevelControl(effectiveId); if (isMustGenerateBeginEndDelimiters) { if (currentOutputInterceptor.getDelimiterNamespaceURI() != null) { // Output end delimiter currentOutputInterceptor.outputDelimiter( currentSavedOutput, currentOutputInterceptor.getDelimiterNamespaceURI(), currentOutputInterceptor.getDelimiterPrefix(), currentOutputInterceptor.getDelimiterLocalName(), "xforms-case-begin-end", "xforms-case-end-" + XFormsUtils.namespaceId(containingDocument, effectiveId)); } else { // Output start and end delimiter using xhtml:span final String xhtmlPrefix = handlerContext.findXHTMLPrefix(); currentOutputInterceptor.outputDelimiter( currentSavedOutput, XMLConstants.XHTML_NAMESPACE_URI, xhtmlPrefix, "span", "xforms-case-begin-end", "xforms-case-begin-" + XFormsUtils.namespaceId(containingDocument, effectiveId)); currentOutputInterceptor.outputDelimiter( currentSavedOutput, XMLConstants.XHTML_NAMESPACE_URI, xhtmlPrefix, "span", "xforms-case-begin-end", "xforms-case-end-" + XFormsUtils.namespaceId(containingDocument, effectiveId)); } } } else if (!isVisible) { // Case not visible, restore output controller.setOutput(currentSavedOutput); } }
private String findDefaultSelectedCaseId() { // TODO: Use ElementAnalysis instead final List<Element> caseElements = Dom4jUtils.elements(element(), XFormsConstants.XFORMS_CASE_QNAME); for (final Element caseElement : caseElements) { if (XFormsCaseControl.isDefaultSelected(caseElement)) { // Found first case with selected="true" return XFormsUtils.getElementId(caseElement); } } // Didn't find a case with selected="true" so return first case return XFormsUtils.getElementId(caseElements.get(0)); }
// Test that xxf:attribute elements with @id and @for were created for @Test public void xxformsAttribute() { final Document document = Dom4jUtils.readFromURL( "oxf:/org/orbeon/oxf/xforms/processor/test-form.xml", XMLUtils.ParserConfiguration.PLAIN); final Metadata metadata = new Metadata(); final Document annotatedDocument = new XBLBindings(new IndentedLogger(XFormsServer.logger, ""), null, metadata) .annotateShadowTree(document, "", false); final DocumentWrapper documentWrapper = new DocumentWrapper(annotatedDocument, null, XPathCache.getGlobalConfiguration()); // Check there is an xxf:attribute for "html" with correct name List<Object> result = XPathCache.evaluate( documentWrapper, "//xxf:attribute[@for = 'html']", XFormsStaticStateImpl.BASIC_NAMESPACE_MAPPING(), null, null, null, null, null); assertNotNull(result); assertEquals(1, result.size()); Element resultElement = (Element) ((NodeWrapper) result.get(0)).getUnderlyingNode(); assertTrue(XFormsUtils.getElementId(resultElement).trim().length() > 0); assertEquals("lang", resultElement.attributeValue(XFormsConstants.NAME_QNAME)); // Check there is an xxf:attribute for "span" with correct name result = XPathCache.evaluate( documentWrapper, "//xxf:attribute[@for = 'span']", XFormsStaticStateImpl.BASIC_NAMESPACE_MAPPING(), null, null, null, null, null); assertNotNull(result); assertEquals(1, result.size()); resultElement = (Element) ((NodeWrapper) result.get(0)).getUnderlyingNode(); assertTrue(XFormsUtils.getElementId(resultElement).trim().length() > 0); assertEquals("style", resultElement.attributeValue(XFormsConstants.NAME_QNAME)); }
@Override public void outputAjaxDiff( PipelineContext pipelineContext, ContentHandlerHelper ch, XFormsControl other, AttributesImpl attributesImpl, boolean isNewlyVisibleSubtree) { // Output regular diff super.outputAjaxDiff(pipelineContext, ch, other, attributesImpl, isNewlyVisibleSubtree); // Output itemset diff if (mustSendItemsetUpdate(pipelineContext, (XFormsSelect1Control) other)) { ch.startElement( "xxf", XFormsConstants.XXFORMS_NAMESPACE_URI, "itemset", new String[] {"id", XFormsUtils.namespaceId(containingDocument, getEffectiveId())}); { final Itemset itemset = getItemset(pipelineContext); if (itemset != null) { final String result = itemset.getJSONTreeInfo(pipelineContext, null, false, getLocationData()); if (result.length() > 0) ch.text(result); } } ch.endElement(); } }
private String getOtherSelectedCaseEffectiveId(XFormsSwitchControl switchControl1) { if (switchControl1 != null && switchControl1.isRelevant()) { final String selectedCaseId = ((XFormsSwitchControlLocal) switchControl1.getInitialLocal()).selectedCaseControlId; return XFormsUtils.getRelatedEffectiveId(switchControl1.getEffectiveId(), selectedCaseId); } else { return null; } }
/** * Get the effective id of the currently selected case. * * @return effective id */ public String getSelectedCaseEffectiveId() { if (isRelevant()) { final XFormsSwitchControlLocal local = (XFormsSwitchControlLocal) getCurrentLocal(); if (local.selectedCaseControlId != null) { return XFormsUtils.getRelatedEffectiveId(getEffectiveId(), local.selectedCaseControlId); } else { throw new OXFException("Selected case was not set for xf:switch: " + getEffectiveId()); } } else { return null; } }
public void execute( XFormsActionInterpreter actionInterpreter, PropertyContext propertyContext, String targetId, XFormsEventObserver eventObserver, Element actionElement, XBLBindings.Scope actionScope, boolean hasOverriddenContext, Item overriddenContext) { final XFormsContainingDocument containingDocument = actionInterpreter.getContainingDocument(); final String controlIdAttributeValue = XFormsUtils.namespaceId(containingDocument, actionElement.attributeValue("control")); if (controlIdAttributeValue == null) throw new OXFException("Missing mandatory 'control' attribute on xforms:control element."); final String resolvedControlStaticId; { // Resolve AVT resolvedControlStaticId = actionInterpreter.resolveAVTProvideValue( propertyContext, actionElement, controlIdAttributeValue, true); if (resolvedControlStaticId == null) return; } final Object controlObject = actionInterpreter.resolveEffectiveControl( propertyContext, actionElement, resolvedControlStaticId); if (controlObject instanceof XFormsControl) { // Dispatch event to control object containingDocument.dispatchEvent( propertyContext, new XFormsFocusEvent(containingDocument, (XFormsEventTarget) controlObject)); } else { // "If there is a null search result for the target object and the source object is an XForms // action such as // dispatch, send, setfocus, setindex or toggle, then the action is terminated with no // effect." final IndentedLogger indentedLogger = actionInterpreter.getIndentedLogger(); if (indentedLogger.isDebugEnabled()) indentedLogger.logDebug( "xforms:setfocus", "control does not refer to an existing control element, ignoring action", "control id", resolvedControlStaticId); } }
@Override public void handleControlEnd( String uri, String localname, String qName, Attributes attributes, String effectiveId, XFormsControl control) throws SAXException { final ElementHandlerController controller = handlerContext.getController(); if (!handlerContext.isNoScript()) { // Restore output controller.setOutput(currentSavedOutput); // Delimiter: end repeat outputInterceptor.flushCharacters(true, true); final boolean isMustGenerateBeginEndDelimiters = !handlerContext.isFullUpdateTopLevelControl(effectiveId); if (isMustGenerateBeginEndDelimiters) { outputInterceptor.outputDelimiter( currentSavedOutput, outputInterceptor.getDelimiterNamespaceURI(), outputInterceptor.getDelimiterPrefix(), outputInterceptor.getDelimiterLocalName(), "xforms-group-begin-end", "group-end-" + XFormsUtils.namespaceId(containingDocument, effectiveId)); } } else if (isNonRelevant(control)) { // In noscript, group was not visible, restore output controller.setOutput(currentSavedOutput); } // Don't support help, alert, or hint! }
@Override public void outputAjaxDiff( ContentHandlerHelper ch, XFormsControl other, AttributesImpl attributesImpl, boolean isNewlyVisibleSubtree) { // Output regular diff super.outputAjaxDiff(ch, other, attributesImpl, isNewlyVisibleSubtree); // Output switch-specific diff if needed only final XFormsSwitchControl otherSwitchControl = (XFormsSwitchControl) other; if (!compareSelectedCase(otherSwitchControl)) { if (isRelevant()) { // Output newly selected case id final String selectedCaseEffectiveId = getSelectedCaseEffectiveId(); assert selectedCaseEffectiveId != null; ch.element( "xxf", XFormsConstants.XXFORMS_NAMESPACE_URI, "div", new String[] { "id", XFormsUtils.namespaceId(containingDocument(), selectedCaseEffectiveId), "visibility", "visible" }); if (otherSwitchControl != null && otherSwitchControl.isRelevant()) { // Used to be relevant, simply output deselected case ids final String previousSelectedCaseId = getOtherSelectedCaseEffectiveId(otherSwitchControl); assert previousSelectedCaseId != null; ch.element( "xxf", XFormsConstants.XXFORMS_NAMESPACE_URI, "div", new String[] { "id", XFormsUtils.namespaceId(containingDocument(), previousSelectedCaseId), "visibility", "hidden" }); } else { // Control was not relevant, send all deselected to be sure // TODO: This should not be needed because the repeat template should have a reasonable // default. final List<XFormsCaseControl> children = getChildrenCases(); if (children != null && children.size() > 0) { for (final XFormsCaseControl caseControl : children) { if (!caseControl.getEffectiveId().equals(selectedCaseEffectiveId)) { ch.element( "xxf", XFormsConstants.XXFORMS_NAMESPACE_URI, "div", new String[] { "id", XFormsUtils.namespaceId(containingDocument(), caseControl.getEffectiveId()), "visibility", "hidden" }); } } } } } } }
private boolean compareSelectedCase(XFormsSwitchControl otherSwitchControl) { return XFormsUtils.compareStrings( getSelectedCaseEffectiveId(), getOtherSelectedCaseEffectiveId(otherSwitchControl)); }
@Override public Map<String, String> serializeLocal() { // Serialize case id return Collections.singletonMap( "case-id", XFormsUtils.getStaticIdFromId(getSelectedCaseEffectiveId())); }
private void gatherInputDependencies( PipelineContext pipelineContext, XFormsContainingDocument containingDocument, IndentedLogger indentedLogger, Stage1CacheableState stage1CacheableState) { final String forwardSubmissionHeaders = XFormsProperties.getForwardSubmissionHeaders(containingDocument); // Add static instance source dependencies for top-level models // TODO: check all models/instances final XFormsStaticState staticState = containingDocument.getStaticState(); for (final Model model : staticState.getModelsForScope(staticState.getXBLBindings().getTopLevelScope())) { for (final Instance instance : model.instancesMap().values()) { if (instance.dependencyURL() != null) { final String resolvedDependencyURL = XFormsUtils.resolveServiceURL( pipelineContext, containingDocument, instance.element(), instance.dependencyURL(), ExternalContext.Response.REWRITE_MODE_ABSOLUTE); if (!instance.isCacheHint()) { stage1CacheableState.addReference( null, resolvedDependencyURL, instance.xxformsUsername(), instance.xxformsPassword(), instance.xxformsPassword(), forwardSubmissionHeaders); if (indentedLogger.isDebugEnabled()) indentedLogger.logDebug( "", "adding document cache dependency for non-cacheable instance", "instance URI", resolvedDependencyURL); } else { // Don't add the dependency as we don't want the instance URI to be hit // For all practical purposes, globally shared instances must remain constant! if (indentedLogger.isDebugEnabled()) indentedLogger.logDebug( "", "not adding document cache dependency for cacheable instance", "instance URI", resolvedDependencyURL); } } } } // Set caching dependencies if the input was actually read // TODO: check all models/instances // Q: should use static dependency information instead? what about schema imports and instance // replacements? for (final XFormsModel currentModel : containingDocument.getModels()) { // Add schema dependencies final String[] schemaURIs = currentModel.getSchemaURIs(); // TODO: We should also use dependencies computed in XFormsModelSchemaValidator.SchemaInfo if (schemaURIs != null) { for (final String currentSchemaURI : schemaURIs) { if (indentedLogger.isDebugEnabled()) indentedLogger.logDebug( "", "adding document cache dependency for schema", "schema URI", currentSchemaURI); stage1CacheableState.addReference( null, currentSchemaURI, null, null, null, forwardSubmissionHeaders); // TODO: support username / password on schema refs } } } // TODO: Add @src attributes from controls? Not used often. // Set caching dependencies for XBL inclusions { final XFormsStaticState.Metadata metadata = containingDocument.getStaticState().getMetadata(); final Set<String> includes = metadata.getBindingsIncludes(); if (includes != null) { for (final String include : includes) { stage1CacheableState.addReference(null, "oxf:" + include, null, null, null, null); } } } }
private void doIt( final PipelineContext pipelineContext, XMLReceiver xmlReceiver, final URIProcessorOutputImpl processorOutput, String outputName) { final ExternalContext externalContext = XFormsUtils.getExternalContext(pipelineContext); final IndentedLogger indentedLogger = XFormsContainingDocument.getIndentedLogger( XFormsToXHTML.logger, XFormsServer.getLogger(), LOGGING_CATEGORY); // ContainingDocument and XFormsState created below final XFormsContainingDocument[] containingDocument = new XFormsContainingDocument[1]; final boolean[] cachedStatus = new boolean[] {false}; final Stage2CacheableState stage2CacheableState; if (TEST_STATE == null) { // Read and try to cache the complete XForms+XHTML document with annotations stage2CacheableState = (Stage2CacheableState) readCacheInputAsObject( pipelineContext, getInputByName(INPUT_ANNOTATED_DOCUMENT), new CacheableInputReader() { public Object read( PipelineContext pipelineContext, ProcessorInput processorInput) { // Compute annotated XForms document + static state document final Stage1CacheableState stage1CacheableState = new Stage1CacheableState(); final Stage2CacheableState stage2CacheableState; final XFormsStaticState[] staticState = new XFormsStaticState[1]; { // Store dependencies container in state before reading ((Stage2TransientState) XFormsToXHTML.this.getState(pipelineContext)) .stage1CacheableState = stage1CacheableState; // Read static state from input stage2CacheableState = readStaticState( pipelineContext, externalContext, indentedLogger, staticState); } // Create containing document and initialize XForms engine // NOTE: Create document here so we can do appropriate analysis of caching // dependencies final XFormsURIResolver uriResolver = new XFormsURIResolver( XFormsToXHTML.this, processorOutput, pipelineContext, INPUT_ANNOTATED_DOCUMENT, URLGenerator.DEFAULT_HANDLE_XINCLUDE); containingDocument[0] = new XFormsContainingDocument( pipelineContext, staticState[0], stage2CacheableState.getAnnotatedTemplate(), uriResolver); // Gather set caching dependencies gatherInputDependencies( pipelineContext, containingDocument[0], indentedLogger, stage1CacheableState); return stage2CacheableState; } @Override public void foundInCache() { cachedStatus[0] = true; } }, false); TEST_STATE = DO_TEST_STATE ? stage2CacheableState : null; } else { stage2CacheableState = TEST_STATE; } try { // Create containing document if not done yet if (containingDocument[0] == null) { assert cachedStatus[0]; // In this case, we found the static state digest and more in the cache, but we must now // create a new XFormsContainingDocument from this information indentedLogger.logDebug( "", "annotated document and static state digest obtained from cache", "digest", stage2CacheableState.getStaticStateDigest()); final XFormsStaticState staticState; { final XFormsStaticState cachedState = XFormsStaticStateCache.instance() .getDocument(pipelineContext, stage2CacheableState.getStaticStateDigest()); if (cachedState != null && cachedState.getMetadata().checkBindingsIncludes()) { // Found static state in cache indentedLogger.logDebug("", "found up-to-date static state by digest in cache"); staticState = cachedState; } else { // Not found static state in cache OR it is out of date, create static state from input // NOTE: In out of date case, could clone static state and reprocess instead? if (cachedState != null) indentedLogger.logDebug("", "found out-of-date static state by digest in cache"); else indentedLogger.logDebug("", "did not find static state by digest in cache"); final StaticStateBits staticStateBits = new StaticStateBits( pipelineContext, externalContext, indentedLogger, stage2CacheableState.getStaticStateDigest()); staticState = new XFormsStaticState( pipelineContext, staticStateBits.staticStateDocument, stage2CacheableState.getStaticStateDigest(), staticStateBits.metadata); // Store in cache XFormsStaticStateCache.instance().storeDocument(pipelineContext, staticState); } } final XFormsURIResolver uriResolver = new XFormsURIResolver( XFormsToXHTML.this, processorOutput, pipelineContext, INPUT_ANNOTATED_DOCUMENT, URLGenerator.DEFAULT_HANDLE_XINCLUDE); containingDocument[0] = new XFormsContainingDocument( pipelineContext, staticState, stage2CacheableState.getAnnotatedTemplate(), uriResolver); } else { assert !cachedStatus[0]; indentedLogger.logDebug( "", "annotated document and static state digest not obtained from cache."); } // Output resulting document if (outputName.equals("document")) { // Normal case where we output XHTML outputResponseDocument( pipelineContext, externalContext, indentedLogger, stage2CacheableState.getAnnotatedTemplate(), containingDocument[0], xmlReceiver); } else { // Output in test mode testOutputResponseState( pipelineContext, containingDocument[0], indentedLogger, xmlReceiver); } // Notify state manager XFormsStateManager.instance().afterInitialResponse(pipelineContext, containingDocument[0]); } catch (Throwable e) { indentedLogger.logDebug("", "throwable caught during initialization."); throw new OXFException(e); } }
public static String getItemId(String effectiveId, String itemIndex) { return XFormsUtils.appendToEffectiveId(effectiveId, "$$e" + itemIndex); }
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; }