使用XSLT的展平树视图结构将节点结构保持为连接元素

时间:2016-07-22 08:11:21

标签: xml xslt tree

我有以下xml(简化结构)

<Workflow>
    <CreateEntity />
    <Activity Type="Custom" />
    <Activity Type="Condition">
        <Activity Type="ConditionBranch">
            <UpdateEntity />
        </Activity>
        <Activity Type="ConditionBranch">
            <Activity Type="Custom" />
        </Activity>
    </Activity>
    <Activity Type="Custom" />
</Workflow>

并希望以这种方式改变它

<WorkflowProcess>
    <Activities>
        <!-- static start with known id -->
        <Activity Id="StartId" />

        <!-- activity from CreateEntity node; id - new GUID -->
        <Activity Id="CreateEntityId" />
        <!-- activity from Custom activity node; id - new GUID -->
        <Activity Id="Custom1Id" />
        <!-- so on -->
        <Activity Id="ConditionId" />
        <Activity Id="UpdateEntityId" />
        <Activity Id="Custom2Id" />
        <Activity Id="Custom3Id" />

        <!-- static end with known id -->
        <Activity Id="EndId" />
    </Activities>
    <Connections>
        <Connection Id="new-guid" From="StartId" To="CreateEntityId"/>
        <Connection Id="new-guid" From="CreateEntityId" To="Custom1Id"/>
        <Connection Id="new-guid" From="Custom1Id" To="ConditionId"/>
        <Connection Id="new-guid" From="ConditionId" To="UpdateEntityId"/>
        <Connection Id="new-guid" From="ConditionId" To="Custom2Id"/>
        <Connection Id="new-guid" From="UpdateEntityId" To="Custom3Id"/>
        <Connection Id="new-guid" From="Custom2Id" To="Custom3Id"/>
        <Connection Id="new-guid" From="Custom3Id" To="EndId"/>
    </Connections>
</WorkflowProcess>

我已经写了最简单的部分 - 获取活动列表并坚持创建连接。

问题是如何构建通过新创建的Id引用我的活动的Connections节点?

我的样本XSL就像(简化)

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:myCustomCode="urn:myExtension">

  <xsl:output method="xml" indent="yes"/>

  <xsl:param name="startActivityId" select="myCustomCode:NewId()"/>
  <xsl:param name="endActivityId" select="myCustomCode:NewId()"/>

  <xsl:template match="/">
    <WorkflowProcess>
        <Activities>
            <!-- start -->
            <Activity Id="{$startActivityId}" />

            <xsl:apply-templates select="Workflow"/>

            <!-- end -->
            <Activity Id="{$endActivityId}" />
        </Activities>
        <Connections>
            <!-- ???-->
            <!-- how to compose these connections? -->
        </Connections>
    </WorkflowProcess>
  </xsl:template>

  <xsl:template match="Workflow">
    <xsl:apply-templates select="CreateEntity"/>
    <xsl:apply-templates select="UpdateEntity"/>
    <xsl:apply-templates select="Activity[@Type='Custom']"/>
    <xsl:apply-templates select="Activity[@Type='Condition']"/>
  </xsl:template>

  <xsl:template match="CreateEntity">
    <xsl:variable name="activityId" select="myCustomCode:NewId()"/>
    <Activity Id="{$activityId}" />
  </xsl:template>

  <xsl:template match="UpdateEntity">
    <xsl:variable name="activityId" select="myCustomCode:NewId()"/>
    <Activity Id="{$activityId}" />
  </xsl:template>

  <xsl:template match="Activity[@Type='Custom']">
    <xsl:variable name="activityId" select="myCustomCode:NewId()"/>
    <Activity Id="{$activityId}" />
  </xsl:template>

  <xsl:template match="Activity[@Type='Condition']">
    <xsl:variable name="activityId" select="myCustomCode:NewId()"/>
    <Activity Id="{$activityId}" />
    <xsl:apply-templates select="Activity[@Type='ConditionBranch']"/>
  </xsl:template>

  <xsl:template match="Activity[@Type='ConditionBranch']">
    <xsl:apply-templates select="Workflow"/>
  </xsl:template>

</xsl:stylesheet>

更新1: 这是一个描述第一个/源xml(和目标也是)

的图表

sample diagram

更新2: 尝试将连接规则形式化已经出现在这样的图表中(例如,添加了另一个活动)

enter image description here

更新3: 这是我的第一次尝试:在全局脚本对象中累积连接。由于XSLT中的变量是不可变的,我们无法修改它们,因此我使用全局对象来存储连接(请参阅脚本元素)。所以当我找到一个新活动时,我会在这里将它连接到全局对象。它允许我以连接方式递归构建所有活动。

修改XSL:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet
    version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt"
    xmlns:myCustomCode="urn:myExtension"
    exclude-result-prefixes="msxsl">

  <msxsl:script implements-prefix="myCustomCode" language="C#">
    <msxsl:using namespace="System.Text" />
    <![CDATA[    

      public string NewId()
      {
        return Guid.NewGuid().ToString();
      }

      private StringBuilder _connections = new StringBuilder();

      public void AppendConnection(string from, string to)
      {
        _connections.AppendFormat("<Connection Id='{0}' From='{1}' To='{2}' />", NewId(), from, to);
      }

      public string GetConnections()
      {
        return _connections.ToString();
      }


    ]]>
  </msxsl:script>

    <xsl:output method="xml" indent="yes"/>

    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
    </xsl:template>


  <xsl:param name="startActivityId" select="myCustomCode:NewId()"/>
  <xsl:param name="endActivityId" select="myCustomCode:NewId()"/>

  <xsl:template match="/">
    <WorkflowProcess>
      <Activities>
        <!-- start -->
        <Activity Id="{$startActivityId}" name="start" />

        <xsl:apply-templates select="Workflow"/>

        <!-- end -->
        <Activity Id="{$endActivityId}" name="end" />
      </Activities>

      <Connections>
        <!-- output connections from script global value -->
        <xsl:value-of select="myCustomCode:GetConnections()" disable-output-escaping="yes" />
      </Connections>

    </WorkflowProcess>
  </xsl:template>

  <xsl:template match="Workflow | Activity[@Type='ConditionBranch']" name="Workflow">
    <xsl:apply-templates select="CreateEntity"/>
    <xsl:apply-templates select="UpdateEntity"/>
    <xsl:apply-templates select="Activity[@Type='Custom']"/>
    <xsl:apply-templates select="Activity[@Type='Condition']"/>
  </xsl:template>

  <xsl:template match="CreateEntity">
    <xsl:variable name="activityId" select="myCustomCode:NewId()"/>
    <Activity Id="{$activityId}" Type='CreateEntity' />

    <!-- build connection to parent -->
    <xsl:call-template name="buildConnection">
      <xsl:with-param name="childId" select = "$activityId" />
    </xsl:call-template>
  </xsl:template>

  <xsl:template match="UpdateEntity">
    <xsl:variable name="activityId" select="myCustomCode:NewId()"/>
    <Activity Id="{$activityId}" Type='UpdateEntity' />

    <!-- build connection to parent -->
    <xsl:call-template name="buildConnection">
      <xsl:with-param name="childId" select = "$activityId" />
    </xsl:call-template>
  </xsl:template>

  <xsl:template match="Activity[@Type='Custom']">
    <xsl:variable name="activityId" select="myCustomCode:NewId()"/>
    <Activity Id="{$activityId}" Type='Custom' />

    <!-- build connection to parent -->
    <xsl:call-template name="buildConnection">
      <xsl:with-param name="childId" select = "$activityId" />
    </xsl:call-template>
  </xsl:template>

  <xsl:template match="Activity[@Type='Condition']">
    <xsl:variable name="activityId" select="myCustomCode:NewId()"/>
    <Activity Id="{$activityId}" Type='Condition' />
    <xsl:apply-templates select="Activity[@Type='ConditionBranch']"/>

    <!-- build connection to parent -->
    <xsl:call-template name="buildConnection">
      <xsl:with-param name="childId" select = "$activityId" />
    </xsl:call-template>
  </xsl:template>

  <!-- find parent and add connection to global script variable -->
  <xsl:template name="buildConnection">
    <xsl:param name = "childId" />

    <!-- the main trick is to get parent id here and pass to my custom function -->

    <xsl:apply-templates select="myCustomCode:AppendConnection($childId, 'parentId')"/>
  </xsl:template>

</xsl:stylesheet>

2 个答案:

答案 0 :(得分:0)

我做了以下尝试来实施您的规则。在进一步移动之前,我想知道在您可能拥有的任何场景中,这是否正确生成了所需的连接。当条件嵌套时,我怀疑它可能不会。

<强> XML

<Workflow>
    <CreateEntity name="A"/>
    <Activity name="B" Type="Custom" />
    <Activity name="C" Type="Condition">
        <Activity name="C1" Type="ConditionBranch">
            <UpdateEntity name="C1A" />
        </Activity>
        <Activity name="C2" Type="ConditionBranch">
            <Activity name="C2A" Type="Custom" />
            <Activity name="C2B" Type="Custom" />
        </Activity>
    </Activity>
    <Activity name="D" Type="Custom" />
</Workflow>

<强> XSLT

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:template match="/Workflow">
    <WorkflowProcess>
        <Activities/>
        <Connections>
            <xsl:apply-templates select="*" mode="conections"/>
        </Connections>
    </WorkflowProcess>  
</xsl:template>

<xsl:template match="*" mode="conections">
    <xsl:choose>
        <xsl:when test="self::CreateEntity">
            <Connection From="0" To="{@name}"/>
        </xsl:when>
        <xsl:when test="preceding-sibling::*[1]/@Type='Condition'">
            <xsl:variable name="name" select="@name" />
            <xsl:for-each select="preceding-sibling::*[1]/*/*[last()]">
                <Connection From="{@name}" To="{$name}"/>
            </xsl:for-each>
        </xsl:when>
        <xsl:when test="@Type='ConditionBranch'"/>
        <xsl:when test="not(preceding-sibling::*)">
            <Connection From="{ancestor::*[2]/@name}" To="{@name}"/>
        </xsl:when>
        <xsl:otherwise>
            <Connection From="{preceding-sibling::*[1]/@name}" To="{@name}"/>
        </xsl:otherwise>        
    </xsl:choose>
    <xsl:apply-templates mode="conections"/>
</xsl:template> 

</xsl:stylesheet>

<强>结果

<?xml version="1.0" encoding="UTF-8"?>
<WorkflowProcess>
   <Activities/>
   <Connections>
      <Connection From="0" To="A"/>
      <Connection From="A" To="B"/>
      <Connection From="B" To="C"/>
      <Connection From="C" To="C1A"/>
      <Connection From="C" To="C2A"/>
      <Connection From="C2A" To="C2B"/>
      <Connection From="C1A" To="D"/>
      <Connection From="C2B" To="D"/>
   </Connections>
</WorkflowProcess>

答案 1 :(得分:0)

虽然最终的解决方案非常复杂,但简化版本是:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet
    version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt"
    xmlns:myCustomCode="urn:myExtension"
    exclude-result-prefixes="msxsl">

  <xsl:output method="xml" indent="yes"/>
  <xsl:strip-space elements="*"/>

  <msxsl:script implements-prefix="myCustomCode" language="C#">
    <msxsl:using namespace="System.Collections.Generic" />
    <![CDATA[    

      public string NewId()
      {
        return Guid.NewGuid().ToString();
      }

      private Dictionary<string, string> _ids = new Dictionary<string, string>();

      /* Converts XSL generated ids to GUIDs */
      public string GetId(string xsl_id)
      {
        if (!_ids.ContainsKey(xsl_id))
        {
          _ids.Add(xsl_id, NewId());
        }

        return _ids[xsl_id];
      }          
    ]]>
  </msxsl:script>

  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:param name="startActivityId" select="myCustomCode:NewId()"/>
  <xsl:param name="endActivityId" select="myCustomCode:NewId()"/>

  <xsl:template match="/">
    <WorkflowProcess>
      <Activities>
        <!-- start -->
        <Activity Id="{$startActivityId}" name="start" />

        <xsl:apply-templates select="Workflow"/>

        <!-- end -->
        <Activity Id="{$endActivityId}" name="end" />
      </Activities>

      <Connections>
        <xsl:apply-templates select="*" mode="conections"/>
      </Connections>

    </WorkflowProcess>
  </xsl:template>

  <xsl:template match="Workflow | Activity[@Type='ConditionBranch']" name="Workflow">
    <xsl:apply-templates select="CreateEntity"/>
    <xsl:apply-templates select="UpdateEntity"/>
    <xsl:apply-templates select="Activity[@Type='Custom']"/>
    <xsl:apply-templates select="Activity[@Type='Condition']"/>
  </xsl:template>

  <xsl:template match="CreateEntity">
    <xsl:variable name="activityId" select="myCustomCode:GetId(generate-id())"/>
    <Activity Id="{$activityId}" Type='CreateEntity' name="{@name}" />
  </xsl:template>

  <xsl:template match="UpdateEntity">
    <xsl:variable name="activityId" select="myCustomCode:GetId(generate-id())"/>
    <Activity Id="{$activityId}" Type='UpdateEntity' name="{@name}" />
  </xsl:template>

  <xsl:template match="Activity[@Type='Custom']">
    <xsl:variable name="activityId" select="myCustomCode:GetId(generate-id())"/>
    <Activity Id="{$activityId}" Type='Custom' name="{@name}" />
  </xsl:template>

  <xsl:template match="Activity[@Type='Condition']">
    <xsl:variable name="activityId" select="myCustomCode:GetId(generate-id())"/>
    <Activity Id="{$activityId}" Type='Condition' name="{@name}" />
    <xsl:apply-templates select="Activity[@Type='ConditionBranch']"/>
  </xsl:template>

  <!-- Connections -->
  <xsl:template match="CreateEntity | UpdateEntity | Activity[@Type='Custom'] | Activity[@Type='Condition']" mode="conections">

      <!-- attach the first element to starting activity -->
      <xsl:if test="local-name(parent::node()) = 'Workflow' and position() = 1">
        <Connection From="{$startActivityId}" To="{myCustomCode:GetId(generate-id(.))}"/>
      </xsl:if>

      <!-- a first element in a condition branch attached to the condition  -->
      <xsl:if test="parent::node()/@Type='ConditionBranch' and position() = 1">
        <Connection From="{myCustomCode:GetId(generate-id(ancestor::*[2]))}" To="{myCustomCode:GetId(generate-id(.))}"/>
      </xsl:if>

      <!-- the last element attached to terminator (ending activity) -->
      <xsl:if test="local-name(parent::node()) = 'Workflow' and position() = last()">
        <xsl:choose>
          <!-- if a last element in workflow is condition attach every its last activities to terminator -->
          <xsl:when test="@Type='Condition'">
            <!-- select only last leaf-nodes of the condition -->
            <xsl:for-each select="descendant::*/*[last()][not(child::*)]">
              <Connection From="{myCustomCode:GetId(generate-id(.))}" To="{$endActivityId}"/>
            </xsl:for-each>
          </xsl:when>
          <xsl:otherwise>
            <Connection From="{myCustomCode:GetId(generate-id(.))}" To="{$endActivityId}"/>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:if>

      <xsl:choose>
        <!-- if previous element is Condition, attach to its last activities -->
        <xsl:when test="preceding-sibling::*[1]/@Type='Condition'">
          <xsl:variable name="childId" select="myCustomCode:GetId(generate-id(.))" />
          <xsl:for-each select="preceding::*/*[last()][not(child::*)]">
            <Connection From="{myCustomCode:GetId(generate-id(.))}" To="{$childId}"/>
          </xsl:for-each>
        </xsl:when>
        <!-- attach to preceding sibling -->
        <xsl:otherwise>
          <xsl:if test="position() > 1">
            <Connection From="{myCustomCode:GetId(generate-id(preceding-sibling::*[1]))}" To="{myCustomCode:GetId(generate-id(.))}"/>
          </xsl:if>
        </xsl:otherwise>
      </xsl:choose>

    <xsl:if test="@Type='Condition'">
      <xsl:apply-templates mode="conections"/>
    </xsl:if>
  </xsl:template>

</xsl:stylesheet>

注意,generate-id()函数在转换期间始终为特定节点返回相同的id。

另一点要注意如何将条件子活动连接到以下条件兄弟元素或终止符。

最后一个:我使用字典将生成的XSL ID转换为脚本块中的GUID。

非常感谢@ michael.hor257k的帮助。