为 OTN 撰稿
为 Oracle 技术网撰写技术文章,获得报酬的同时可提升技术技能。
更多信息
密切关注
OTN 架构师社区
OTN ArchBeat 博客 Facebook Twitter YouTube 随身播图标

Oracle Identity Manager:实现 Additional Request Information(有关请求的更多信息)特性

作者:Nitin Patel

就如何在 Oracle Identity Manager 中实现 Additional Request Information 特性提供了详细的分步技术指导

2013 年 4 月

下载
download-icon13-1OracleIdentityManager
download-icon13-1SampleAdditionalReqInfoApp
download-icon13-1sandbox-AdditionalRequestInformat

简介

Oracle Identity Manager (OIM) 11.1.2.2.0 版(R2PS2 版)引入了 Additional Request Information 特性。与此特性有关的文档可从这里获取。默认(现成)情况下,该特性在 OIM UI 中并不可用,必须由客户实现。实现涉及一些 UI 自定义,包括(但不限于)为 UI 自定义创建沙箱、开发自定义应用开发框架 (ADF) 任务流,以及在二者之间建立链接。

本文旨在帮助 OIM 客户、顾问和任何想要实现此特性的其他人,重点关注整体实现方法,以及为此特性开发一个分步示例实现。本文所提供示例包括示例沙箱 zip、Jdeveloper ADF 应用(含源码)以及 ADF 库 jar(含自定义任务流和托管 bean (mBean))。

注:本文假定读者熟悉 OIM 和 ADF 开发,了解这里所载 OIM UI 自定义的方方面面。

了解 Additional Request Information 特性实现

站在一定的高度看,此特性能够通过 UI 自定义指定与请求(用户操作)相关的其他信息。其他信息有助于请求审批决策或合规性。请参见本文末尾“用例”一节中所记载的示例用例。

要实现此特性,必须执行以下操作:

  1. 使用 Oracle Web Composer (OWC) 创建一个沙箱,执行 UI 自定义,在购物车提交页面中添加自定义组件(命令链接或按钮)。
     
  2. 开发和部署自定义的 ADF 有界任务流,其中包含提交购物车时要收集的其他信息。
     
  3. 将在上面步骤 B 中创建的自定义任务流与步骤 A 中添加的自定义组件(命令链接)关联。
     

该特性一旦实现,请求者就会看到步骤 A 中在购物车提交页面中添加的自定义 UI 组件(命令链接/按钮)。单击该链接/按钮,即会启动步骤 B 中部署的自定义 ADF 有界任务流。请求者可在任务流中指定以 UI 输入组件形式显示的其他信息,也可以保存信息。该信息作为请求的一部分保存,可供跟踪请求的请求者(或其他可以查看请求的用户)查看。审批者可以查看和编辑请求者输入的信息,对分配给他们的相应审批任务采取适当的操作。

A. 创建沙箱

使用 Oracle Web Composer 创建沙箱(如文档中所述),执行 UI 自定义,在购物车提交页面中添加自定义组件(命令链接或按钮)。具体步骤如下:

  1. xelsysadm 用户身份登录 OIM 自助控制台。
     
  2. 创建角色(如 role1),默认情况下,该角色将发布到顶层组织。
     
  3. 创建并激活一个沙箱(如 AdditionalRequestInformation)。
     
  4. 导航到 Catalog,搜索上面创建的 role1,将其添加到购物车,并单击 Checkout 进入购物车提交页面。
     
  5. 单击 Customize 自定义购物车提交页面。
     
  6. 单击 Editing Page: Identity Self Service 下的 View 下拉菜单,选择 Source
     
  7. 编辑 Cart Details panelHeader,添加 commandLink 组件(用于 Additional Cart Information),如图 1 和图 2 所示。
    patel-oim-request-info-fig01
    图 1
    patel-oim-request-info-fig02
    图 2

     
  8. 同样,在购物车项表中添加另一个 commandLink(用于 Additional Cart Item Information),如图 3 所示。
    patel-oim-request-info-fig03
    图 3


    单击 Cart ItemsDisplay Name 列中的 Remove 按钮,快速导航到 Composer 中的源代码,如图 4 所示。
    patel-oim-request-info-fig04
    图 4

     
  9. 单击购物车提交 UI 上的 Submit 按钮,找到 Composer 中对应的源组件。您将看到 commandToolbarButton:Update,该按钮已经与 Submit 按钮一起配置,默认情况下禁用,如图 5 所示。
    patel-oim-request-info-fig05
    图 5

     
  10. 选择禁用的 Update 按钮,单击 Edit 打开弹出窗口,显示 Update 按钮的属性。在此弹出窗口中,编辑 show component 属性,这将显示另一个弹出窗口,默认值为:#{backingBeanScope.cartReqBean.updateEnbled}(参见图 6)。
    patel-oim-request-info-fig06
    图 6

     
  11. 将表达式语言 (EL) 表达式(图 6 中突出显示)替换为 True,以便将 Update 按钮的这一自定义添加到沙箱中。通过这一自定义 (show component = true),购物车提交页面中也将显示 Update 按钮;确保该值已修改,根据自定义的 EL 表达式显示 Update 按钮。

    注:自定义 Update 按钮时要小心,所做修改将影响按钮显示时的默认行为。除非审批者需要更新有关请求的其他信息,否则跳过此自定义。
     
  12. 导出沙箱。我们稍后将(在下面的“自定义沙箱”一节)修改沙箱。
     

B. 开发和部署自定的 ADF 任务流

开发一个自定义的 ADF 有界任务流,将其随 oracle.iam.ui.custom-dev-starter-pack.war 一起部署。该任务流应包含提交购物车时要收集的所有其他信息。此外,还可以开发和部署任何自定义的托管 bean(参见开发托管 Bean 和任务流),在其中包含自定义逻辑来实现根据购物车项有选择地显示/隐藏链接等用例。

以下步骤显示如何开发实现与“Additional Request information”特性有关的自定义所需的自定义 ADF 构件,以及如何将其作为 oracle.iam.ui.custom-dev-starter-pack.war 的一部分一起部署。这些说明是对此特性的官方文档的补充。

按照文档 30.10.1 节中的说明开发一个 ADF View Controller 项目。如果开发受阻,请参考以下步骤/屏幕截图。

  1. 创建通用的应用。
    patel-oim-request-info-fig07
    图 7

     
  2. 向应用中添加新的 ADF View Controller 项目。注:此示例使用默认软件包 oracle.iam.ui.custom
    patel-oim-request-info-fig08
    图 8
    patel-oim-request-info-fig09
    图 9

     
  3. 将 OIM 库(OIM Client 库、OIM Model 共享库、OIM View 共享库)和 ADF Model Runtime 库添加到项目类路径。
    patel-oim-request-info-fig10
    图 10

     
  4. 为新创建的 ViewController 项目定义部署配置文件,存档类型为 ADF Library JAR File,并命名(如 adflibAdditionalReqInfoPrj1)。
    patel-oim-request-info-fig11
    图 11

     

到目前为止,我们所执行的步骤与文档 30.10.1 节中相同。

下面一组步骤显示如何创建有界 ADF 任务流,其中包含将在购物车提交时收集的其他信息。作为本次练习的一部分,我们将:

  1. 创建一个名为“addnl-entitlement-info.xml”的有界任务流示例。
     
  2. 创建一个名为“additionalEntitlementInfo.jsff”的页面片段,它是此任务流的一部分。
     
  3. 在上面的页面片段中包含购物车提交过程中要收集的其他信息。我们将在页面片段中包含 Start DateEnd Date 域,以及 SaveCancel 按钮,以便保存用户输入的其他信息或取消/关闭任务流。
     
  4. 创建两个托管 bean,后面将在任务流对其进行配置:AddnlEntitlementInfoStateBeanAddnlEntitlementInfoRequestBeanAddnlEntitlementInfoStateBeanpageFlowScope 中配置,其中保存 jsff 的状态(Start Date 和 End Date)。AddnlEntitlementInfoRequestBeanBackingBean 作用域中配置,它保存与 jsff 中 SaveCancel 按钮对应的操作监听器方法 onSaveonCancel
     
  5. 创建一个名为 Util.java 的 Java 类,用于定义任何实用程序方法;创建一个 HelperBean.java 托管 bean,用于自定义逻辑。项目结构如下所示:
     
patel-oim-request-info-fig12
图 12

下面一组步骤显示如何创建 ADF View Controller 项目的每个组成构件。

  1. 右键单击 Project > New > ADF Taskflow,在 <Project_dir>/public_html/WEB-INF 下创建一个新的 ADF 任务流。在下面的屏幕截图中,在 <Project_dir>/public_html/WEB-INF/oracle/iam/ui/custom/additionalInfo 目录下创建了任务流 addnl-entitlement-info.xml
     
  2. patel-oim-request-info-fig13
    图 13
  3. 创建两个 Java 类 (Project > New > Java Class),下面将在第 7 步将其配置为任务流托管 bean。此外,创建另一个 java 类 Util,它拥有实用程序方法。
    1. 下面的屏幕截图显示一个为名为 AddnlEntitlementInfoStateBean 的托管 bean,用于表示任务流的状态。也就是说,它包含任务流域 startDateEndDate。注意,该类必须实现 Serializable 接口。

      该类还有几个其他实例变量:requestFormContextadditionalRequestInfo,对应于下面在第 8 步中讨论的任务流输入参数。readOnly 是一个布尔变量,用于确定 jsff 中的 start date 和 end date 域是只读还是可编辑。cartItemId 用作 additionalRequestInfo 对象的索引,以提取与所选购物车项对应的其他信息(参见接口)。

      该类具有为所有实例变量生成的访问器方法。它还有一个公有 initialize 方法,使用 requestFormContext 对象确定和初始化 readOnlycartItemId 域,以及使用 additionalRequestInfo 对象确定和初始化 startDate 和 endDate 域。

      名为 store 的公有方法使用 startDateendDate 域的新/更新值更新 additionalRequestInfo 对象。在 additionalEntitlementInfo.jsff 页面上单击 Save 按钮时,应调用该方法。
       
    2. patel-oim-request-info-fig14
      图 14
    3. 图 15 对应于 AddnlEntitlementInfoRequestBean。它有一个名为 stateBeanAddnlEntitlementInfoStateBean 类型成员变量,是因任务流中配置的托管 bean 属性(在下面第 7 步中)而注入的。注意,访问器方法是使用 JDeveloper 为此变量生成的。操作监听器方法 onSave 调用上面在状态 bean 中定义的 store 方法,将状态保存到任务流/jsff。然后使用下述 Util 类隐藏弹出窗口。操作监听器方法 onCancel 隐藏弹出窗口,不保存状态。
       
    4. patel-oim-request-info-fig15
      图 15
    5. 图 16 显示 Util 类,其实用程序方法 findParentComponent 用于上面的请求 bean。
       

     
  4. patel-oim-request-info-fig16
    图 16
  5. addnl-entitlement-info.xml 任务流中将上面创建的状态 bean 和请求 bean 配置为托管 bean,如下面的屏幕截图所示。注意,stateBeanpageFlow 作用域中配置,requestBeanbackingBean 作用域中配置。选择 requestBean,使用名称 stateBean、类型 oracle.iam.ui.custom.AdditionalRequestInfo.AddnlEntitlementInfoStateBean 和值 '#{pageFlowScope.stateBean}' 配置托管属性。这是为了将 stateBean 注入 requestBean
     
  6. patel-oim-request-info-fig17
    图 17
  7. 定义强制性任务流输入参数 additionalRequestInfo(类:oracle.iam.ui.common.model.catalog.requestdetails.AdditionalRequestInfo)和 requestFormContext(类:oracle.iam.ui.platform.view.RequestFormContext),如图 18 所示。将这些输入参数存储在上面创建的 AddnlEntitlementInfoStateBean 中对应的成员变量中。这可以使用 EL 表达式 #{pageFlowScope.stateBean.additionalRequestInfo}#{pageFlowScope.stateBean.requestFormContext} 实现。注意,EL 表达式中使用的 stateBean 对应于上面配置的托管 bean。
     
  8. patel-oim-request-info-fig18
    图 18
  9. 以设计模式打开 addnl-entitlement-info.xml 任务流,拖放一个 Method 调用活动,并为其命名(如 initialize)。要将其标记为默认活动,请右键单击 Mark Activity > Default Activity。双击该活动,将其与 stateBean 中的 initialize 方法关联。

    拖放一个 View 组件,将其命名为 additionalEntitlementInfo。双击该组件,创建一个名为 additionalEntitlementInfo.jsff 的页面片段。

    initializeadditionalEntitlementInfo 活动之间添加一个 Control Flow Case,如下面的 addnl-entitlement-info.xml 设计和源码中所示:
     
  10. patel-oim-request-info-fig19
    图 19
    patel-oim-request-info-fig20
    图 20
  11. 双击打开视图/jsff,并包含所有 UI 组件(Start Date、End Date、Save、Cancel),如下面的 jsff 屏幕截图中的设计和源代码所示:
     
patel-oim-request-info-fig21
图 21
patel-oim-request-info-fig22
图 22

注:对于 Start Date inputDate 组件,readOnly 属性设置为 #{pageFlowScope.stateBean.readOnly},value 属性设置为 #{pageFlowScope.stateBean.startDate}。这会将此组件及其属性绑定到 stateBean (AddnlEntitlementInfoStateBean) 中的变量。同样的操作也适用于 End Date 组件。还要注意,Save 按钮的 action listener 属性设置为 #{backingBeanScope.requestBean.onSave},这将其绑定到 requestBeanonSave 方法。

保存所有更改。

  1. 创建新的 Java 类(如 HelperBean.java),以扩展 oracle.iam.ui.platform.view.backing.BaseMB。该类用于实现文档 30.11.5 和 30.11.6 节中讨论的用例,也就是说,用于动态确定购物车提交页面中的以下内容:
    1. 是否在请求/购物车级别显示自定义的 Additional Information 链接。
       
    2. 是否在购物车项级别显示自定义的 Additional Information 链接。
       
    3. 是否在 Approval 详细信息页面上显示 Update 按钮,供审批者更新请求中的 Additional Information,然后审批任务。
       
    4. 确定在运行时单击请求/购物车级别链接启动哪个自定义任务流作为弹出窗口。
       
    5. 确定在运行时单击(购物车项表中)特定购物车项的自定义链接时启动哪个自定义任务流作为弹出窗口。
       
    要实现类似上述用例,可以开发一个自定义的托管 bean,如下面的屏幕截图中所示。该类包括以下几个主要方法:
    1. initialize:该方法使用以下代码初始化 oracle.iam.ui.platform.view.RequestFormContext 类型的实例变量 requestFormContext

      requestFormContext = RequestFormContext.newInstance(null).getCurrentContext();

      所有其他 helper 方法均可调用此方法初始化 requestFormContext,然后可使用后者获取与当前购物车/请求有关的上下文。
       
    2. isRenderAdditionalDetailsForRequest:该方法可用于确定是否必须在请求/购物车级别显示 Additional Information 链接。
       
    3. isRenderAdditionalDetailsForCartItem:该方法可用于确定是否必须为当前购物车项显示 Additional Information 链接。
       
    4. isRenderUpdateButton:该方法可用于确定是否在审批详细信息页面上显示 Update 按钮。
       

     
patel-oim-request-info-fig23
图 23

图 24 捕获几个方法的代码。注意,产品文档的 30.11.530.11.6 节已经介绍了可用于实现所需自定义的数据。还介绍了 cartItemDataMappageFlowScope 等。

patel-oim-request-info-fig24
图 24
  1. 在项目的 adfc-config.xml 中,将上面创建的 HelperBean 类配置为托管 bean(在 backingBean 作用域中),如图 25 所示。
     
patel-oim-request-info-fig25
图 25

注意,该 bean(及其任何公有方法)可在任何 EL 表达式中用名称 additionalInfoHelperBean 引用。例如:#{backingBeanScope.additionalInfoHelperBean.renderUpdateButton}

  1. 保存所有更改,然后右键单击 Project > Deploy 并选择 adflibAdditionalReqInfoPrj1 进行部署。单击 Finish 完成部署。编译和部署成功后,即创建一个类似以下的 jar:<JDEV_WORKSPACE>/AdditionalReqInfoApp/AdditionalReqInfoPrj/deploy/adflibAdditionalReqInfoPrj1.jar
     
  2. 构建 ADF 库 jar 之后,将其作为 oracle.iam.ui.custom-dev-starter-pack.war 的一部分部署,如文档 30.10.4 节所述。
     

保护自定义任务流

按照文档 30.5.1 节中的介绍使用 Authorization Policy Manager (APM) 为自定义任务流添加权限。

注意,本练习以上部分所构建的示例任务流为:/WEB-INF/oracle/iam/ui/custom/additionalInfo/addnl-entitlement-info.xml#addnl-entitlement-info

oracle.adf.controller.security.TaskFlowPermission 自定义或查看资源名 /WEB-INF/oracle/iam/ui/custom/.* 是现成内置的,因此无需为示例任务流添加任何新的权限。

C. 自定义沙箱

在此步骤中,我们将对上面步骤 A 中创建并导出的沙盒进行自定义。在自定义 UI 组件与自定义任务流之间建立链接并作为步骤 B 的一部分将其部署,也需要此步骤。

  1. 完成 A 中步骤之后,您应已导出一个沙箱 zip 文件。我们将其称为“基本沙箱”,它只捕获通过 UI composer 执行的自定义。如果您解压缩 zip 并查看 cart-details.jsff.xml 文件,将看到类似以下的 mds:insertmds:modify 标记:
     
<mds:insert parent="ph1" position="last">
  <af:commandLink xmlns:af="http://xmlns.oracle.com/adf/faces/rich" id="e9847224848"
   shortDesc="Additional Cart Information" text="Additional Cart Information"/>
</mds:insert>
<mds:insert parent="pgl4" position="first">
  <af:commandLink xmlns:af="http://xmlns.oracle.com/adf/faces/rich" id="e2712655339"
   text="Additional Cart Item Information" shortDesc="Additional Cart Item Information"/>
</mds:insert>
<mds:modify element="ctb2">
  <mds:attribute name="rendered" value="true"/>
</mds:modify> 

确保文件中包含全部三个自定义:添加两个新 commandLink、修改 Update 按钮。

  1. 参考文档编辑这个基本沙箱,将自定义 UI 组件与步骤 B 中开发的自定义任务流关联。对基本沙箱 zip 中的 cart-details.jsff.xml 文件做以下更改:
    • 将两个 commandLinkactionlistener 属性设置为与下面完全相同的值:
      actionListener="#{catalogRequestBean.launchAdditionalRequestInfoTaskFlow}"
       
    • 设置两个 commandLinkclientAttributetaskFlowId 属性的值应为完全限定任务流名称。在本示例中,我们将 taskflowId 属性设置为 EL 表达式:
      #{backingBeanScope.additionalInfoHelperBean.additionalInfoTaskFlowIdForCartItem}

      此 EL 表达式调用已经开发并作为 ADF 库 jar 的一部分部署的 HelperBean 类的 getAdditionalInfoTaskFlowIdForCartItem 方法。该方法返回以下 taskflowId/WEB-INF/oracle/iam/ui/custom/additionalInfo/addnl-entitlement-info.xml#addnl-entitlement-info
       
    • 修改 Update 按钮 rendered 属性的值(在 下面)。注意,值为“true”表示在请求创建/详细信息和审批详细信息任务流过程中将看到此按钮。为避免这种情况,应添加与示例沙箱中类似的 EL:
      #{backingBeanScope.additionalInfoHelperBean.renderUpdateButton}

      此 EL 表达式调用前面开发并作为 ADF 库 jar 的一部分部署的 HelperBean 类的 isRenderUpdateButton 方法。该方法只对审批更新流返回“true”,因此其他流上不会显示该按钮。

       

     
  2. 因此,示例 cart-details.jsff.xml 文件如下所示:
     
<?xml version='1.0' encoding='UTF-8'?>
<mds:customization version="11.1.1.64.93" xmlns:mds="http://xmlns.oracle.com/mds" 
motype_local_name="root" motype_nsuri="http://java.sun.com/JSP/Page">
   <mds:insert parent="ph1" position="last">
      <af:commandLink xmlns:af="http://xmlns.oracle.com/adf/faces/rich" id="e81172800" 
	  text="Additional Cart Information" 
	  actionListener="#{catalogRequestBean.launchAdditionalRequestInfoTaskFlow}" 
	  rendered="false"/>
   </mds:insert>
   <mds:insert parent="pgl4" position="first">
   	<af:commandLink xmlns:af="http://xmlns.oracle.com/adf/faces/rich" id="e5139681203" 
	text="Additional Cart Item Information" 
	actionListener="#{catalogRequestBean.launchAdditionalRequestInfoTaskFlow}" 
rendered="#{backingBeanScope.additionalInfoHelperBean.renderAdditionalDetailsForCartItem}">
	   <af:clientAttribute xmlns:af="http://xmlns.oracle.com/adf/faces/rich" 
name="taskFlowId" 
value="#{backingBeanScope.additionalInfoHelperBean.additionalInfoTaskFlowIdForCartItem}"/>
           <af:clientAttribute xmlns:af="http://xmlns.oracle.com/adf/faces/rich" 
name="dialogTitleIcon" value="/images/request_ena.png"/>
           <af:clientAttribute xmlns:af="http://xmlns.oracle.com/adf/faces/rich" 
name="headerText" value="Additional Cart item Information"/>
	   <af:clientAttribute xmlns:af="http://xmlns.oracle.com/adf/faces/rich" 
name="dialogTitle" value="#{backingBeanScope.additionalInfoHelperBean.popupTitle}"/>
	</af:commandLink>
   </mds:insert>
   <mds:modify element="ctb2">
      <mds:attribute name="rendered" 
	  value="#{backingBeanScope.additionalInfoHelperBean.renderUpdateButton}"/>
   </mds:modify>
</mds:customization>
注:
  • “Additional Cart Item Information”链接上的 rendered 属性设置为如下 EL 表达式:
    rendered=""#{backingBeanScope.additionalInfoHelperBean.renderAdditionalDetailsForCartItem}

    此 EL 表达式调用前面开发并作为 ADF 库 jar 的一部分部署的 HelperBean 类的 isRenderAdditionalDetailsForCartItem 方法。该方法保存逻辑,仅当角色名称为 role1 时返回“true”。
     
  • Additional Cart Information 链接的 rendered 属性设置为“false”。这意味着该链接永远不会在请求/购物车级别显示。使用 EL 表达式(如上所述)动态确定是否显示该链接。
     
  • 对其他属性使用类似 EL 表达式,只是对应的方法在 HelperBean 类或任何其他 backingBean 作用域的自定义托管 bean 中实现。
     
  1. 重新创建修改后的的基本沙箱 zip,将其导入 OIM 并激活。
     

用例

以 xelsysadm 用户身份登录,尝试以下用例:

  1. 添加对角色(如 role1 和 role2)的访问,确认 Additional Cart Item Information 链接在购物车提交页面中只对 role1 可见。
     
  2. 单击购物车项表中的 Additional Cart Item Information 链接,确认自定义任务流以弹出窗口的形式启动。在表单的 start date 和 end date 域中输入值(图 26)。
     
patel-oim-request-info-fig26
图 26
  1. 购物车提交成功。
     
  2. 在请求汇总页面中,单击链接,确保请求创建过程中提供的值可见(图 27)。
     
patel-oim-request-info-fig27
图 27
  1. 打开 Inbox,单击为以上请求创建的审批任务。
     
  2. 确认,并确定在购物车项级别提供的其他信息(对 role1)可见并可编辑(图 28)。
     
patel-oim-request-info-fig28
图 28
  1. 确保 Update 按钮可见。编辑一些值,再单击 Update 按钮。审批任务。
     
  2. 打开 Request Details,确保其中反映了审批者更新的其他属性更新值。
     
  3. 其他用例(如 Provision/Revoke 授权)也启动相同的示例任务流。
     
  4. 尝试一个用例,包括为异构实体(角色、授权)添加访问。确保在批量请求级别存储其他请求详细信息,并在稍后审批后将其传播到子请求。
     
  5. 访问 Request Web Service (/reqsvc/reqsvc),调用 getRequestDetails 操作获取上面创建的任何请求的请求详细信息。确保 Web 服务方法返回其他信息。
     

总结

本文演示在 OIM (11.1.2.2.0) 中如何实现 Additional Request information 特性。本练习中开发了一个自定义的有界任务流,并使用同一任务流为角色和授权相关请求指定其他信息。可以使用类似方法开发和部署多个自定义工作流,根据需求的不同,实现文档的 30.11.5 和 30.11.6 节中所述自定义。

参考资料

  1. 本文参考 Oracle Identity Manager 文档中的 UI 自定义部分。
     

关于作者

Nitin Patel 是 Oracle Identity Manager 开发团队的成员,致力于应用服务器安全、与一次性登录解决方案集成、OIM 服务器和 UI 开发等领域。他获得了比尔拉技术与科学学院的技术学士学位。