private Object deserializeInstance( IndentedLogger indentedLogger, boolean isReadonly, boolean isHandleXInclude, ConnectionResult connectionResult) throws Exception { final Object resultingDocument; // 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 try { if (!isReadonly) { // Resulting instance must not be read-only // TODO: What about configuring validation? And what default to choose? resultingDocument = TransformerUtils.readDom4j( connectionResult.getResponseInputStream(), connectionResult.resourceURI, isHandleXInclude, true); if (indentedLogger.isDebugEnabled()) indentedLogger.logDebug("", "deserializing to mutable instance"); } else { // Resulting instance must be read-only // TODO: What about configuring validation? And what default to choose? // NOTE: isApplicationSharedHint is always false when get get here. // isApplicationSharedHint="true" is handled above. resultingDocument = TransformerUtils.readTinyTree( XPathCache.getGlobalConfiguration(), connectionResult.getResponseInputStream(), connectionResult.resourceURI, isHandleXInclude, true); if (indentedLogger.isDebugEnabled()) indentedLogger.logDebug("", "deserializing to read-only instance"); } } catch (Exception e) { throw new XFormsSubmissionException( submission, e, "xforms:submission: exception while reading XML response.", "processing instance replacement", new XFormsSubmitErrorEvent( containingDocument, submission, XFormsSubmitErrorEvent.ErrorType.PARSE_ERROR, connectionResult)); } return resultingDocument; }
private XFormsContainingDocument createDocumentFromStore( RequestParameters parameters, boolean isInitialState, boolean disableUpdates) { final boolean isServerState = parameters.getEncodedClientStaticState() == null; final XFormsState xformsState; if (isServerState) { // State must be found by UUID in the store final ExternalContext externalContext = NetUtils.getExternalContext(); final XFormsStateStore stateStore = XFormsStateStoreFactory.instance(externalContext); if (indentedLogger.isDebugEnabled()) indentedLogger.logDebug( LOG_TYPE, "Getting document state from store.", "current cache size", Integer.toString(XFormsDocumentCache.instance().getCurrentSize()), "current store size", Long.toString(stateStore.getCurrentSize()), "max store size", Long.toString(stateStore.getMaxSize())); final ExternalContext.Session session = externalContext.getRequest().getSession(XFormsStateManager.FORCE_SESSION_CREATION); xformsState = stateStore.findState(session, parameters.getUUID(), isInitialState); if (xformsState == null) { // 2014-11-12: This means that 1. We had a valid incoming session and 2. we obtained a lock // on the // document, yet we didn't find it. This means that somehow state was not placed into or // expired from // the state store. throw new SessionExpiredException( "Unable to retrieve XForms engine state. Unable to process incoming request."); } } else { // State comes directly with request xformsState = new XFormsState( scala.Option.<String>apply(null), parameters.getEncodedClientStaticState(), DynamicState.apply(parameters.getEncodedClientDynamicState())); } // Create document final XFormsContainingDocument document = new XFormsContainingDocument(xformsState, disableUpdates); assert isServerState ? document.getStaticState().isServerStateHandling() : document.getStaticState().isClientStateHandling(); return document; }
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); } }
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 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 void execute( XFormsActionInterpreter actionInterpreter, Element actionElement, Scope actionScope, boolean hasOverriddenContext, Item overriddenContext) { final XFormsContainingDocument containingDocument = actionInterpreter.containingDocument(); // Resolve all attributes as AVTs final String dialogStaticOrEffectiveId = actionInterpreter.resolveAVT(actionElement, "dialog"); final String neighborEffectiveId; { final String neighborStaticOrEffectiveId = actionInterpreter.resolveAVT(actionElement, "neighbor"); final XFormsControl neighbor = (XFormsControl) ((neighborStaticOrEffectiveId != null) ? actionInterpreter.resolveOrFindByEffectiveId( actionElement, neighborStaticOrEffectiveId) : null); neighborEffectiveId = (neighbor != null) ? neighbor.getEffectiveId() : null; } final boolean constrainToViewport; { final String constrain = actionInterpreter.resolveAVT(actionElement, "constrain"); constrainToViewport = !"false".equals(constrain); } if (dialogStaticOrEffectiveId != null) { // Dispatch xxforms-dialog-open event to dialog final Object controlObject = actionInterpreter.resolveOrFindByEffectiveId(actionElement, dialogStaticOrEffectiveId); if (controlObject instanceof XXFormsDialogControl) { final XXFormsDialogControl targetDialog = (XXFormsDialogControl) controlObject; // Remove focus if any containingDocument.setClientFocusEffectiveControlId(null); final XFormsEvent newEvent = new XXFormsDialogOpenEvent( containingDocument, targetDialog, neighborEffectiveId, constrainToViewport); addContextAttributes(actionInterpreter, actionElement, newEvent); targetDialog.getXBLContainer(containingDocument).dispatchEvent(newEvent); // Check if form author has set focus while dialog was opening, and if not focus on dialog final String currentFocusEffectiveId = containingDocument.getClientFocusControlEffectiveId(); if (currentFocusEffectiveId == null && targetDialog.isVisible()) targetDialog .getXBLContainer() .dispatchEvent(new XFormsFocusEvent(containingDocument, targetDialog)); } else { final IndentedLogger indentedLogger = actionInterpreter.indentedLogger(); if (indentedLogger.isDebugEnabled()) indentedLogger.logDebug( "xxforms:show", "dialog does not refer to an existing xxforms:dialog element, ignoring action", "dialog id", dialogStaticOrEffectiveId); } } }
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 XFormsContainingDocument createDocumentFromStore( RequestParameters parameters, boolean isInitialState, boolean disableUpdates) { final boolean isServerState = parameters.getEncodedClientStaticState() == null; final XFormsState xformsState; if (isServerState) { // State must be found by UUID in the store final ExternalContext externalContext = NetUtils.getExternalContext(); final XFormsStateStore stateStore = XFormsStateStoreFactory.instance(externalContext); if (indentedLogger.isDebugEnabled()) indentedLogger.logDebug( LOG_TYPE, "Getting document state from store.", "current cache size", Integer.toString(XFormsDocumentCache.instance().getCurrentSize()), "current store size", Long.toString(stateStore.getCurrentSize()), "max store size", Long.toString(stateStore.getMaxSize())); final ExternalContext.Session session = externalContext.getRequest().getSession(XFormsStateManager.FORCE_SESSION_CREATION); xformsState = stateStore.findState(session, parameters.getUUID(), isInitialState); if (xformsState == null) { // Oops, we couldn't find the state in the store final String UNABLE_TO_RETRIEVE_XFORMS_STATE_MESSAGE = "Unable to retrieve XForms engine state."; final String PLEASE_RELOAD_PAGE_MESSAGE = "Please reload the current page. Note that you will lose any unsaved changes."; // Produce exception final ExternalContext.Session currentSession = externalContext.getRequest().getSession(false); final String message; if (currentSession == null || currentSession.isNew()) { // This means that no session is currently existing, or a session exists but it is newly // created message = "Your session has expired. " + PLEASE_RELOAD_PAGE_MESSAGE; } else { // There is a session and it is still known by the client message = UNABLE_TO_RETRIEVE_XFORMS_STATE_MESSAGE + " " + PLEASE_RELOAD_PAGE_MESSAGE; } indentedLogger.logError("", message); throw new OXFException(message); } } else { // State comes directly with request xformsState = new XFormsState( scala.Option.<String>apply(null), parameters.getEncodedClientStaticState(), DynamicState.apply(parameters.getEncodedClientDynamicState())); } // Create document final XFormsContainingDocument document = new XFormsContainingDocument(xformsState, disableUpdates); assert isServerState ? document.getStaticState().isServerStateHandling() : document.getStaticState().isClientStateHandling(); return document; }
public void execute( XFormsActionInterpreter actionInterpreter, Element actionElement, Scope actionScope, boolean hasOverriddenContext, Item overriddenContext) { final IndentedLogger indentedLogger = actionInterpreter.indentedLogger(); final XFormsContainingDocument containingDocument = actionInterpreter.containingDocument(); final XFormsContextStack contextStack = actionInterpreter.actionXPathContext(); final String atAttribute = actionElement.attributeValue("at"); final String originAttribute = actionElement.attributeValue("origin"); final String contextAttribute = actionElement.attributeValue(XFormsConstants.CONTEXT_QNAME); // Extension: allow position to be an AVT final String resolvedPositionAttribute = actionInterpreter.resolveAVT(actionElement, "position"); // Extension: xxf:default="true" AVT requires that recalculate apply default values on the // inserted nodes. final boolean setRequireDefaultValues = "true" .equals( actionInterpreter.resolveAVT( actionElement, XFormsConstants.XXFORMS_DEFAULTS_QNAME)); // "2. The Node Set Binding node-set is determined." final List<Item> collectionToBeUpdated; { final BindingContext currentBindingContext = contextStack.getCurrentBindingContext(); collectionToBeUpdated = currentBindingContext.newBind() ? currentBindingContext.nodeset() : XFormsConstants.EMPTY_ITEM_LIST; } final boolean isEmptyNodesetBinding = collectionToBeUpdated == null || collectionToBeUpdated.size() == 0; // "1. The insert context is determined." // "The insert action is terminated with no effect if [...] a. The context attribute is not // given and the Node // Set Binding node-set is the empty node-set." if (contextAttribute == null && isEmptyNodesetBinding) { if (indentedLogger.isDebugEnabled()) indentedLogger.logDebug("xf:insert", "context is empty, terminating"); return; } // Handle insert context (with @context attribute) final Item insertContextItem; if (hasOverriddenContext) { // "If the result is an empty nodeset or not a nodeset, then the insert action is terminated // with no effect. " if (overriddenContext == null || !(overriddenContext instanceof NodeInfo)) { if (indentedLogger.isDebugEnabled()) indentedLogger.logDebug( "xf:insert", "overridden context is an empty nodeset or not a nodeset, terminating"); return; } else { insertContextItem = overriddenContext; } } else { insertContextItem = contextStack.getCurrentBindingContext().getSingleItem(); } // "The insert action is terminated with no effect if [...] b. The context attribute is given, // the insert // context does not evaluate to an element node and the Node Set Binding node-set is the empty // node-set." // NOTE: In addition we support inserting into a context which is a document node if (contextAttribute != null && isEmptyNodesetBinding && !DataModel.isElement(insertContextItem) && !DataModel.isDocument(insertContextItem)) { if (indentedLogger.isDebugEnabled()) indentedLogger.logDebug( "xf:insert", "insert context is not an element node and binding node-set is empty, terminating"); return; } // "3. The origin node-set is determined." final List<Item> originObjects; { if (originAttribute == null) { originObjects = null; } else { // There is an @origin attribute // "If the origin attribute is given, the origin node-set is the result of the evaluation of // the // origin attribute in the insert context." originObjects = actionInterpreter.evaluateKeepItems( actionElement, Collections.singletonList(insertContextItem), 1, originAttribute); // "The insert action is terminated with no effect if the origin node-set is the empty // node-set." if (originObjects.size() == 0) { if (indentedLogger.isDebugEnabled()) indentedLogger.logDebug("xf:insert", "origin node-set is empty, terminating"); return; } } } // "4. The insert location node is determined." int insertionIndex; { if (isEmptyNodesetBinding) { // "If the Node Set Binding node-set empty, then this attribute is ignored" insertionIndex = 0; } else if (atAttribute == null) { // "If the attribute is not given, then the default is the size of the Node Set Binding // node-set" insertionIndex = collectionToBeUpdated.size(); } else { // "a. The evaluation context node is the first node in document order from the Node Set // Binding // node-set, the context size is the size of the Node Set Binding node-set, and the context // position is 1." // "b. The return value is processed according to the rules of the XPath function round()" final String insertionIndexString = actionInterpreter.evaluateAsString( actionElement, collectionToBeUpdated, 1, "round(" + atAttribute + ")"); // "c. If the result is in the range 1 to the Node Set Binding node-set size, then the // insert // location is equal to the result. If the result is non-positive, then the insert location // is // 1. Otherwise, the result is NaN or exceeds the Node Set Binding node-set size, so the // insert // location is the Node Set Binding node-set size." // Don't think we will get NaN with XPath 2.0... insertionIndex = "NaN".equals(insertionIndexString) ? collectionToBeUpdated.size() : Integer.parseInt(insertionIndexString); // Adjust index to be in range if (insertionIndex > collectionToBeUpdated.size()) insertionIndex = collectionToBeUpdated.size(); if (insertionIndex < 1) insertionIndex = 1; } } final String normalizedPosition; { if (resolvedPositionAttribute == null) { // Default value normalizedPosition = "after"; } else if ("after".equals(resolvedPositionAttribute) || "before".equals(resolvedPositionAttribute)) { // Specified value normalizedPosition = resolvedPositionAttribute; } else { // Invalid value if (indentedLogger.isInfoEnabled()) indentedLogger.logWarning( "xf:insert", "invalid position attribute, defaulting to \"after\"", "value", resolvedPositionAttribute); normalizedPosition = "after"; } } doInsert( containingDocument, indentedLogger, normalizedPosition, collectionToBeUpdated, (NodeInfo) insertContextItem, originObjects, insertionIndex, true, true, setRequireDefaultValues); }
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; }