public String getEncodedDocument() {
      final Document eventsDocument = Dom4jUtils.createDocument();
      final Element eventsElement = eventsDocument.addElement(XFormsConstants.XXFORMS_EVENTS_QNAME);

      final Element eventElement = eventsElement.addElement(XFormsConstants.XXFORMS_EVENT_QNAME);
      eventElement.addAttribute("name", eventName);
      eventElement.addAttribute("source-control-id", targetEffectiveId);
      eventElement.addAttribute("bubbles", Boolean.toString(bubbles));
      eventElement.addAttribute("cancelable", Boolean.toString(cancelable));

      return XFormsUtils.encodeXML(eventsDocument, false);
    }
    public void toJSON(StringBuilder sb, long currentTime) {
      sb.append('{');
      sb.append("\"delay\":");
      sb.append(getTime() - currentTime);
      if (isMaxDelay()) {
        sb.append(",\"discardable\":true");
      }
      sb.append(",\"show-progress\":");
      sb.append(isShowProgress());
      if (isShowProgress()) {
        sb.append(",\"progress-message\":\"");
        XFormsUtils.escapeJavaScript(getProgressMessage());
        sb.append('"');
      }
      sb.append(",\"event\":\"");
      sb.append(getEncodedDocument());
      sb.append('"');

      sb.append("}");
    }
  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);
    }
  }
  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);
        }
      }
    }
  }
Exemplo n.º 5
0
  public static List<NodeInfo> doInsert(
      XFormsContainingDocument containingDocument,
      IndentedLogger indentedLogger,
      String positionAttribute,
      List collectionToBeUpdated,
      NodeInfo insertContextNodeInfo,
      List<Item> originItems,
      int insertionIndex,
      boolean doClone,
      boolean doDispatch,
      boolean requireDefaultValues) {

    final boolean isEmptyNodesetBinding =
        collectionToBeUpdated == null || collectionToBeUpdated.size() == 0;

    final NodeInfo insertLocationNodeInfo;
    if (isEmptyNodesetBinding) {
      // Insert INTO a node

      // "If the Node Set Binding node-set is not specified or empty, the insert location node is
      // the insert
      // context node."

      // "a. If the Node Set Binding node-set is not specified or empty, the target location depends
      // on the
      // node type of the cloned node. If the cloned node is an attribute, then the target location
      // is before
      // the first attribute of the insert location node. If the cloned node is not an attribute,
      // then the
      // target location is before the first child of the insert location node."

      insertLocationNodeInfo = insertContextNodeInfo;
    } else {
      // Insert BEFORE or AFTER a node
      insertLocationNodeInfo = (NodeInfo) collectionToBeUpdated.get(insertionIndex - 1);
    }

    // Identify the instance that actually changes
    final XFormsInstance modifiedInstanceOrNull =
        (containingDocument != null)
            ? containingDocument.getInstanceForNode(insertLocationNodeInfo)
            : null;

    // NOTE: The check on `hasAnyCalculationBind` is not optimal: we should check whether
    // specifically there are any xxf:default which can touch this
    // instance, ideally.
    // NOTE: We do this test here so that we don't unnecessarily annotate nodes.
    final boolean applyDefaults =
        requireDefaultValues
            && modifiedInstanceOrNull != null
            && modifiedInstanceOrNull.model().staticModel().hasDefaultValueBind()
            && containingDocument
                .getXPathDependencies()
                .hasAnyCalculationBind(
                    modifiedInstanceOrNull.model().staticModel(),
                    modifiedInstanceOrNull.getPrefixedId());

    // "3. The origin node-set is determined."
    // "5. Each node in the origin node-set is cloned in the order it appears in the origin
    // node-set."
    final List<Node> sourceNodes;
    final List<Node> clonedNodes;
    {
      final List<Node> clonedNodesTemp;
      if (originItems == null) {
        // There are no explicitly specified origin objects, use node from Node Set Binding node-set

        // "If the origin attribute is not given and the Node Set Binding node-set is empty, then
        // the origin
        // node-set is the empty node-set. [...] The insert action is terminated with no effect if
        // the
        // origin node-set is the empty node-set."

        if (isEmptyNodesetBinding) {
          if (indentedLogger != null && indentedLogger.isDebugEnabled())
            indentedLogger.logDebug(
                "xf:insert", "origin node-set from node-set binding is empty, terminating");
          return Collections.emptyList();
        }

        // "Otherwise, if the origin attribute is not given, then the origin node-set consists of
        // the last
        // node of the Node Set Binding node-set."
        final Node singleSourceNode =
            XFormsUtils.getNodeFromNodeInfoConvert(
                (NodeInfo) collectionToBeUpdated.get(collectionToBeUpdated.size() - 1));
        // TODO: check namespace handling might be incorrect. Should use
        // copyElementCopyParentNamespaces() instead?
        final Node singleClonedNode = Dom4jUtils.createCopy(singleSourceNode);

        sourceNodes = Collections.singletonList(singleSourceNode);
        clonedNodesTemp = Collections.singletonList(singleClonedNode);
      } else {
        // There are explicitly specified origin objects

        // "The insert action is terminated with no effect if the origin node-set is the empty
        // node-set."
        if (originItems.size() == 0) {
          if (indentedLogger != null && indentedLogger.isDebugEnabled())
            indentedLogger.logDebug("xf:insert", "origin node-set is empty, terminating");
          return Collections.emptyList();
        }

        // "Each node in the origin node-set is cloned in the order it appears in the origin
        // node-set."

        sourceNodes = new ArrayList<Node>(originItems.size()); // set to max possible size
        clonedNodesTemp = new ArrayList<Node>(originItems.size());

        for (final Object currentObject : originItems) {
          if (currentObject instanceof NodeInfo) {
            // This is the regular case covered by XForms 1.1 / XPath 1.0

            // NOTE: Don't clone nodes if doClone == false
            final Node sourceNode =
                XFormsUtils.getNodeFromNodeInfoConvert((NodeInfo) currentObject);
            final Node clonedNode =
                doClone
                    ? (sourceNode instanceof Element)
                        ? ((Element) sourceNode).createCopy()
                        : (Node) sourceNode.clone()
                    : sourceNode;

            sourceNodes.add(sourceNode);
            clonedNodesTemp.add(clonedNode);

          } else if (currentObject instanceof AtomicValue) {
            // This is an extension: support sequences containing atomic values

            // Convert the result to a text node
            final String stringValue = ((Item) currentObject).getStringValue();
            final Text textNode = DocumentFactory.createText(stringValue);

            sourceNodes.add(
                null); // there is no source node for this cloned node, it's a source item
            clonedNodesTemp.add(textNode);
          } else throw new IllegalStateException();
        }
      }

      // Remove instance data from cloned nodes and perform Document node adjustment
      for (int i = 0; i < clonedNodesTemp.size(); i++) {
        final Node clonedNodeTemp = clonedNodesTemp.get(i);

        if (clonedNodeTemp instanceof Element) {
          // Element node
          if (applyDefaults) InstanceDataOps.setRequireDefaultValueRecursively(clonedNodeTemp);
          else InstanceDataOps.removeRecursively(clonedNodeTemp);
          clonedNodeTemp.detach();
        } else if (clonedNodeTemp instanceof Attribute) {
          // Attribute node
          if (applyDefaults) InstanceDataOps.setRequireDefaultValueRecursively(clonedNodeTemp);
          else InstanceDataOps.removeRecursively(clonedNodeTemp);
          clonedNodeTemp.detach();
        } else if (clonedNodeTemp instanceof Document) {
          // Document node
          final Element clonedNodeTempRootElement = clonedNodeTemp.getDocument().getRootElement();

          if (clonedNodeTempRootElement == null) {
            // Can be null in rare cases of documents without root element
            clonedNodesTemp.set(
                i, null); // we support having a null node further below, so set this to null
          } else {
            if (applyDefaults)
              InstanceDataOps.setRequireDefaultValueRecursively(clonedNodeTempRootElement);
            else InstanceDataOps.removeRecursively(clonedNodeTempRootElement);
            // We can never really insert a document into anything at this point, but we assume that
            // this means the root element
            clonedNodesTemp.set(i, clonedNodeTempRootElement.detach());
          }
        } else {
          // Other nodes
          clonedNodeTemp.detach();
        }
      }
      clonedNodes = clonedNodesTemp;
    }

    // "6. The target location of each cloned node or nodes is determined"
    // "7. The cloned node or nodes are inserted in the order they were cloned at their target
    // location
    // depending on their node type."

    // Find actual insertion point and insert
    final int insertLocationIndexWithinParentBeforeUpdate;
    final List<Node> insertedNodes;
    final String beforeAfterInto;
    if (isEmptyNodesetBinding) {
      // Insert INTO a node

      insertLocationIndexWithinParentBeforeUpdate =
          findNodeIndexRewrapIfNeeded(insertLocationNodeInfo);

      final Node insertLocationNode =
          XFormsUtils.getNodeFromNodeInfo(insertContextNodeInfo, CANNOT_INSERT_READONLY_MESSAGE);
      insertedNodes = doInsert(insertLocationNode, clonedNodes, modifiedInstanceOrNull, doDispatch);
      beforeAfterInto = "into";

      // Normalize text nodes if needed to respect XPath 1.0 constraint
      {
        boolean hasTextNode = false;
        for (Node clonedNode : clonedNodes) {
          hasTextNode |= clonedNode != null && (clonedNode instanceof Text);
        }
        if (hasTextNode) Dom4jUtils.normalizeTextNodes(insertLocationNode);
      }
    } else {
      // Insert BEFORE or AFTER a node

      insertLocationIndexWithinParentBeforeUpdate =
          findNodeIndexRewrapIfNeeded(insertLocationNodeInfo);

      final Node insertLocationNode =
          XFormsUtils.getNodeFromNodeInfo(insertLocationNodeInfo, CANNOT_INSERT_READONLY_MESSAGE);
      final Document insertLocationNodeDocument = insertLocationNode.getDocument();
      if (insertLocationNodeDocument != null
          && insertLocationNodeDocument.getRootElement() == insertLocationNode) {

        // "c. if insert location node is the root element of an instance, then that instance root
        // element
        // location is the target location. If there is more than one cloned node to insert, only
        // the
        // first node that does not cause a conflict is considered."

        insertedNodes =
            doInsert(
                insertLocationNode.getDocument(), clonedNodes, modifiedInstanceOrNull, doDispatch);
        beforeAfterInto = positionAttribute; // TODO: ideally normalize to "into document node"?

        // NOTE: Don't need to normalize text nodes in this case, as no new text node is inserted
      } else {
        // "d. Otherwise, the target location is immediately before or after the insert location
        // node, based on the position attribute setting or its default."

        if (insertLocationNode instanceof Attribute) {
          // Special case for "next to an attribute"

          // NOTE: In XML, attributes are unordered. dom4j handles them as a list so has order, but
          // the XForms spec shouldn't rely on attribute order. We could try to keep the order, but
          // it
          // is harder as we have to deal with removing duplicate attributes and find a reasonable
          // insertion strategy.

          // TODO: Don't think we should even do this now in XForms 1.1
          insertedNodes =
              doInsert(
                  insertLocationNode.getParent(), clonedNodes, modifiedInstanceOrNull, doDispatch);

        } else {
          // Other node types
          final Element parentNode = insertLocationNode.getParent();
          final List<Node> siblingElements = parentNode.content();
          final int actualIndex = siblingElements.indexOf(insertLocationNode);

          // Prepare insertion of new element
          final int actualInsertionIndex;
          if ("before".equals(positionAttribute)) {
            actualInsertionIndex = actualIndex;
          } else {
            // "after"
            actualInsertionIndex = actualIndex + 1;
          }

          // "7. The cloned node or nodes are inserted in the order they were cloned at their target
          // location depending on their node type."

          boolean hasTextNode = false;
          int addIndex = 0;
          insertedNodes = new ArrayList<Node>(clonedNodes.size());
          for (Node clonedNode : clonedNodes) {

            if (clonedNode != null) { // NOTE: we allow passing some null nodes so we check on null
              if (!(clonedNode instanceof Attribute || clonedNode instanceof Namespace)) {
                // Element, text, comment, processing instruction node
                siblingElements.add(actualInsertionIndex + addIndex, clonedNode);
                insertedNodes.add(clonedNode);
                hasTextNode |= clonedNode instanceof Text;
                addIndex++;
              } else {
                // We never insert attributes or namespace nodes as siblings
                if (indentedLogger != null && indentedLogger.isDebugEnabled())
                  indentedLogger.logDebug(
                      "xf:insert",
                      "skipping insertion of node as sibling in element content",
                      "type",
                      Node$.MODULE$.nodeTypeName(clonedNode),
                      "node",
                      clonedNode instanceof Attribute
                          ? Dom4jUtils.attributeToDebugString((Attribute) clonedNode)
                          : clonedNode.toString());
              }
            }
          }

          // Normalize text nodes if needed to respect XPath 1.0 constraint
          if (hasTextNode) Dom4jUtils.normalizeTextNodes(parentNode);
        }

        beforeAfterInto = positionAttribute;
      }
    }

    // Whether some nodes were inserted
    final boolean didInsertNodes = insertedNodes != null && insertedNodes.size() > 0;

    // Log stuff
    if (indentedLogger != null && indentedLogger.isDebugEnabled()) {
      if (didInsertNodes)
        indentedLogger.logDebug(
            "xf:insert",
            "inserted nodes",
            "count",
            Integer.toString(insertedNodes.size()),
            "instance",
            (modifiedInstanceOrNull != null) ? modifiedInstanceOrNull.getEffectiveId() : null);
      else indentedLogger.logDebug("xf:insert", "no node inserted");
    }

    // "XForms Actions that change the tree structure of instance data result in setting all four
    // flags to true"
    if (didInsertNodes && modifiedInstanceOrNull != null) {
      // NOTE: Can be null if document into which delete is performed is not in an instance, e.g. in
      // a variable
      modifiedInstanceOrNull.markModified();
      modifiedInstanceOrNull
          .model()
          .markStructuralChange(
              scala.Option.<XFormsInstance>apply(modifiedInstanceOrNull),
              FlaggedDefaultsStrategy$.MODULE$);
    }

    // Gather list of modified nodes
    final List<NodeInfo> insertedNodeInfos;
    if (didInsertNodes && modifiedInstanceOrNull != null) {
      // Instance can be null if document into which delete is performed is not in an instance, e.g.
      // in a variable
      final DocumentWrapper documentWrapper =
          (DocumentWrapper) modifiedInstanceOrNull.documentInfo();
      insertedNodeInfos = new ArrayList<NodeInfo>(insertedNodes.size());
      for (Node insertedNode : insertedNodes)
        insertedNodeInfos.add(documentWrapper.wrap(insertedNode));
    } else {
      insertedNodeInfos = Collections.emptyList();
    }

    // "4. If the insert is successful, the event xforms-insert is dispatched."
    // XFormsInstance handles index and repeat items updates
    if (doDispatch && didInsertNodes && modifiedInstanceOrNull != null) {

      // Adjust insert location node and before/after/into in case the root element was replaced
      final NodeInfo adjustedInsertLocationNodeInfo;
      final String adjustedBeforeAfterInto;

      final NodeInfo parent =
          insertedNodeInfos.get(0).getNodeKind() == org.w3c.dom.Node.ELEMENT_NODE
              ? insertedNodeInfos.get(0).getParent()
              : null;
      if (parent != null && parent.equals(parent.getDocumentRoot())) {
        // Node was inserted under document node
        adjustedInsertLocationNodeInfo = parent.getDocumentRoot();
        adjustedBeforeAfterInto = "into";
      } else {
        adjustedInsertLocationNodeInfo = rewrapIfNeeded(insertLocationNodeInfo);
        adjustedBeforeAfterInto = beforeAfterInto;
      }

      Dispatch.dispatchEvent(
          new XFormsInsertEvent(
              modifiedInstanceOrNull,
              insertedNodeInfos,
              originItems,
              adjustedInsertLocationNodeInfo,
              adjustedBeforeAfterInto,
              insertLocationIndexWithinParentBeforeUpdate));
    }

    return insertedNodeInfos;
  }