OpenSocial 开发实践,第 2 部分: Apache Shindig 概览

–>


引用说明:原文来自于http://www.ibm.com/developerworks/cn/web/1104_sunqy_opensocial2/index.html,为了方便本人阅读,文本格式略有调整。

 


Shindig 简介

Shindig 最初是在 2007 年由 Google 发起的,作为 iGoogle 的 Gadget 容器。Shindig 作为一个参考容器,可以在任意网站上运行支持 OpenSocial 的社交应用。当时 Shindig 支持 Google Gadget 的规范和 OpenSocial 的规范。Shindig 的中文释义为盛大的社交舞会,作为 OpenSocial 规范的一个参考实现的项目名称也是非常合适的。OpenSocial 提供给了开发者一系列通用的 API,基于这些 API 开发的社交应用程序(Gadget、iWidget)可以运行在任意支持 OpenSocial 规范的社交网站上。

Shindig 的主要目的就是在帮助这些社交网站可以在很短的时间内实现对 OpenSocial 规范的支持,从而使得社交应用的开发者可以不用去关心平台的转换。Shindig 自从 2007 年底成为 Apache 的一个开源项目后,就作为 OpenSocial 规范的参考实现和可以支持 OpenSocial 应用的容器不断更新,目前最新发布的版本是 2.0.1,实现了 OpenSocial 1.1 的规范。很多社交网站都是基于 Shindig 实现自己的 OpenSocial 功能的,比如 LinkedIn、hi5。

Shindig 的另一个目标是支持多种语言的实现,目前有 Java 和 PHP 两个版本,Java 版本基于 Java Servlet Stack 实现,本文的一些示例以及代码也都基于 Java 版本。

Shindig 的主要组件包括:

  • Gadget Container JavaScript(Gadget 容器,JavaScript 类库):提供诸如 UI 展现、安全、交互、特性扩展等相关的功能。
  • Gadget Rendering Server(展现 Gadget 的服务器):负责解析 Gadget XML,转换成浏览器使用的 HTML 和 JavaScript。
  • OpenSocial Container JavaScript(OpenSocial 容器的 JavaScript 类库):基于 Gadget 容器的 JavaScript 类库之上,位于客户端的 OpenSocial 容器,提供 OpenSocial 相关的功能,例如存取 People、Activity、AppData 等相关的社交数据。
  • OpenSocial Data Server(OpenSocial 数据服务器):提供了特定于某个容器的服务器端的接口,包括基于 Rest/RPC 协议的 Services,用于存取 People、Activity、AppData 等相关的社交数据,而且提供了清楚的扩展点,其他网站都可以据此实现自己的服务。

Shindig 的组件架构如图 1 所示:

图 1. Shindig 的组件架构图
 

Gadget 由 XML 和其所使用的特性 JavaScript 类库构成,默认的 Gadget 容器会将 Gadget 放在一个 iframe 里面来展现。当 Gadget 容器准备 Render 一个 Gadget 时,首先会获取该 Gadget 的 metadata 信息,进而通过对应的信息组成 iframeUrl,并将该 URL 设置为 iframe 的 src,此时便会触发服务器端名为“xml-to-html”的 servlet 即 Gadget Rendering Sevlet 负责处理这个请求并最终返回 HTML。JsonRpcServlet 和 DataServiceServlet 负责处理 OpenSocial 相关的请求,DataServiceServlet 处理 Rest 请求,JsonRpcServlet 处理 RPC 请求,在后台他们共享同样的实现。OpenSocial Hanlder 负责处理 OpenSocial 相关的请求,具体由下面各个相关的 Service 实现。中间的 JsonDBOpenSocialService 则是一个实现了各个 Service 接口的具体实现,以 Json 文件作为数据源。

Shindig 的项目基于 Maven 构建,共有以下几个子项目(基于 Jave 版本的源码):

  • shindig-common: 该项目主要提供了一些基本和公用的方法。
  • shindig-gadgets: 该项目主要是 Gadget Render 的 Server 端实现,包括解析 XML,解析 Gadget 里面用到的特性,重写内容,返回 HTML 到客户端等。
  • shindig-social-api: 该项目主要是 OpenSocial 相关功能的 Server 端实现,提供了 People,Activity,AppData 等的 Service 和 Handler。
  • shindig-smaples: 该项目是一个用 JSON 实现了 OpenSocial 存取数据的示例。
  • shindig-server: 该项目是整个项目的 Server 端配置,包括 web.xml 等。整个项目构建完成后会生成一个 war 包,作为一个 Web 应用部署到服务器即可。
  • shindig-features: 该项目与语言无关,都是 Client 端的 JavaScript 库,包括 Gadget 容器,OpenSocial 容器所用到的 JavaScript 类库,以及一些用到的特性,本文后面会重点介绍几个常用的特性。
  • shindig-extras: 该项目与语言无关,可以看作是提供给开发者扩展 Shindig 的一个参考。Shindig 的整个项目是基于 Guice 框架的,Guice 是 Google 开发的一个轻量级的依赖注入框架。当用户需要扩展 Shindig 实现自己的功能或特性时,可以与原有的 Shindig 完全隔离,开发自己的 Guice 模块就好了,最后通过 Guice 的依赖注入做相应的配置就可以。

回页首

Shindig 的客户端流程

Shindig 的客户端包括:Gadget 容器、OpenSocial 容器、JSON、Restful 容器和对 Caja 的支持。对应的流程如图 2 所示:

图 2. Shindig Client 端流程图
 

所有这几个容器最终都通过 Gadget.io 的 XmlHttpRequest 发送请求到服务器端。

Gadget 容器目前在 Shindin 里面有两个版本:shindig-container 和 shindig-container 1.0。Shindig 里面所给出的示例都是基于 shindig-container 的,这也是最初 Shindig 的 Gadget 容器。Shindig-container 1.0 是由 Google 在今年 5 月份提交到 Shindig 的,旨在对 Gadget 容器进行更好的分层。本文后面也会对 shindig-container 1.0 进行相应的介绍并给出示例。图 3 所示即为 shindig-container 的组件图:

图 3. Gadget 容器的组件图
 

  • shindig.Container 作为核心的容器,提供了创建 Gadget,添加 Gadget 及呈现 Gadget 的方法。
  • shindig.GadgetServices 提供了设置 Gadget 的高度、标题、用户参数设置等服务。
  • shindig.LayoutManager 提供了获取 Gadget 的布局信息的接口。
  • shindig.Gadget 作为核心的 Gadget 对象,提供了诸如 render、getContent 等方法。根据 Gadget 的特性是否用到 pubsub-2 决定获取的内容,由此需要两种类型的 Gadget。shindig.IfrGadget 和 shindig.OAAIfrGadget。如果不需要此特性,只需要简单的返回一个 iframe 作为主内容即可,如果需要 pubsub-2 则需要创建 openajaxhub 的容器。
  • shindig.UserPrefStore 提供了保存用户参数设置的接口。

使用 shindig-container 来展现一个 Gadget 的 code 如下所示:

清单 1. 使用 Shindig-container 展现 Gadget

				
<html> 
    <head> 
        <title>Sample: A Sample Gadget</title> 
        <link rel="stylesheet" href="gadgets.css"> 
        <script type="text/javascript" src="/gadgets/js/shindig-container:rpc.js?c=1">
        </script> 
        <script type="text/javascript"> 
            var specUrl0 = 'http://localhost:8080/container/helloWorld.xml';

            function init() { 
                shindig.container.layoutManager = 
                new shindig.FloatLeftLayoutManager('gadget-parent');

                var gadget = shindig.container
                    .createGadget({specUrl: specUrl0, width: 500});
                shindig.container.addGadget(gadget); 
            }; 

            function renderGadgets() { 
                shindig.container.renderGadgets(); 
            }; 
        </script> 
    </head> 
    <body onLoad="init();renderGadgets();"> 
        <h2>Sample: A Sample Gadget</h2> 
        <div id="gadget-parent" class="gadgets-gadget-parent"></div>
    </body>
</html>
     

新的 Gadget 容器更加简单方便,对应的组件图如图 4 所示:使用新的 Gadget 容器来展现 Gadget 的 Code 如清单 2 所示:

图 4. 新的 Gadget 容器组件图
 

清单 2. 使用新 container 展现 Gadget

				
<html> 
    <head> 
        <title>Sample: A Sample Gadget with new container</title> 
        <link rel="stylesheet" href="gadgets.css"> 
        <script type="text/javascript" 
            src="/gadgets/js/container:rpc:pubsub-2.js?c=1&container=default">
        </script> 
        <script type="text/javascript"> 

        var my = {}; 

        my.gadgetSpecUrls = [ 
            'http://localhost:8080/container/sample-pubsub-2-publisher.xml', 
            'http://localhost:8080/container/sample-pubsub-2-subscriber.xml'
        ]; 

        my.init = function() { 
            gadgets.pubsub2router.init( 
            { 
                onSubscribe: function(topic, container) { 
                    return true; 
                }, 
                onUnsubscribe: function(topic, container) { 
                }, 
                onPublish: function(topic, data, pcont, scont) { 
                    return true; 
                } 
            }); 
        }; 

        my.renderGadgets = function() { 
            shindig.auth.updateSecurityToken(
                'john.doe:john.doe:appid:shindig:url:0:default');
            var config = {}; 
            config[shindig.container.ServiceConfig.API_PATH] = '/rpc'; 
            config[shindig.container.ContainerConfig.RENDER_DEBUG] = "1"; 
            var myContainer = new shindig.container.Container(config); 

            for (var i = 0; i < my.gadgetSpecUrls.length; ++i) { 
                var el = document.getElementById("gadget-site-" + i); 
                var gadgetSite = myContainer.newGadgetSite(el); 
                myContainer.navigateGadget(gadgetSite, my.gadgetSpecUrls[i], {}, {});
            } 
        }; 
        </script> 
    </head> 
    <body onLoad="my.init();my.renderGadgets();"> 
        <h2>Sample: A Sample Gadget with new container</h2> 
        <div id="gadget-site-0" class="gadgets-gadget-chrome"></div> 
        <div id="gadget-site-1" class="gadgets-gadget-chrome"></div> 
    </body> 
</html> 
     

新的 Gadget 容器相比 shindig-container 更加清晰,使用起来也更加简单,只需要创建一个 gadget site,然后调用 navigateGadget 即可。在 shindig 发布的 2.0.1 版本中,新的 Gadget 容器使用起来还有几个小 bug,如果您想使用最新的 Gadget 容器,请下载本文提供的针对新的 Gadget 容器的 patch。新的 Gadget 容器提供了很好的分层结构和扩展性,用户可以参考该容器实现自己的 Gadget 容器。本系列后面的文章将会介绍如何基于这个新的 Gadget 容器在当前页面上的内嵌,来展现一个 Gadget, 而不是使用 iframe。

OpenSocial 的容器用来创建与 OpenSocial 有关的请求,例如:newFetchPersonRequest、newFetchPeopleRequest、newFetchPersonAppDataRequest 等,定义了诸如 Person、Activity、Phone 等对象。早期的请求通常通过:

var req = opensocial.newDataRequest(); 
req.add(req.newFetchPersonRequest(opensocial.DataRequest.PersonId.VIEWER),'viewer');
req.add(req.newFetchPeopleRequest(opensocial.DataRequest.Group.VIEWER_FRINENDS), 
    'viewerFriends');
req.send(callback); 
     

来发送请求。目前用的比较多的是使用 osapi:

var batch = osapi.newBatch(); 
var fields = ['id','age','name','gender','profileUrl','thumbnailUrl']; 
batch.add('viewer', osapi.people.getViewer({sortBy:'name',fields:fields})); 
batch.add('viewerFriends', 
    osapi.people.getViewerFriends({sortBy:'name',fields:fields})); 
batch.execute(callback); 
	

回页首

Shindig 的服务端流程

Shindig 的服务器端流程主要分为两个核心部分,一个是 Render Gadget,一个是处理 OpenSocial 相关的请求。Render Gadget 由 GadgetRenderingServlet 处理,如图 5 所示:首先调用 doGet 方法,调用 Renderer 的 render 方法,通过 Process 解析出一个 Gadget 实例,而后调用具体的 HTMLRenderer 的 render 方法,核心的就是很多的 Rewriter,通过对 Gadget 里面的内容进行重写来生成 HTML。

  • PipelineDataGadgetRewriter: 如果 Gadget 里面使用了 data-pipelining 的特性,则会调用该 rewriter 生成容器所需要的数据。
  • TemplateRewriter: 如果 Gadget 里面用到了 template 的特性,则会用真实的数据替换 template 里面对应的键值。
  • ProxyingContentRewriter:如果 Gadget 里面请求的内容需要通过 proxy 才能取得,通过该 rewriter 则会强制 Gadget 的请求必须经过代理。
  • CajaContentRewriter:Caja 是 Google 发起的一个项目,旨在制订一个 JavaScript 语言的子集和最佳编程指导方针,约束 JavaScript 程序员编写的代码,符合一个更加安全,更加合理的 JS 代码。如果 Gadget 里面声明了 Caja 特性,则需要通过这个 rewriter 根据 Caja 的要求重写 Gadget 里面的内容。
  • RenderingGadgetRewriter :呈现 Gadget 肯定会用到的一个 Rewriter,主要生成 HTML 的元素,根据 Gadget 声明的特性一一注入所需的 JavaScript 文件。

图 5. 呈现 Gadget 的处理流程
 

处理 OpenSocial 请求的具体流程可参见 Shindig 的组件架构图中所示,分为以下几步:

  1. 调用 JsonRpcServlet 或者 DataServiceServlet 取到对应的 Converter;
  2. 找到合适的 Handler,比如或者好友列表对应的 Handler 为 PeopleHandler;
  3. 从数据库或者文件存储里取到 JSON 对象;
  4. 将返回结果构成一个列表,返回到客户端。

关于服务器端的组件图及各个 Servlet 之间的关系可参考 Shindig 中 REST 实现概述 中的详细介绍。

回页首

Shindig 所提供的 OpenSocial API 介绍

Shindig 提供了两套 OpenSocial 的 API:Rest 和 JSON-RPC 的。下面分别对两种 API 加以介绍:Rest 的 API 主要用于服务器到服务器的交互,JSON-RPC 主要用于 Gadget 跟服务器端的交互,诸如 osapi 的很多服务都是通过调用 JSON-RPC API 完成的。

JSON-RPC 接口

Shindig 实现了所有必须的 RPC 服务,包括 People、Activity、Appdata、Messages 和 System。完整的方法列表请参考 Shindig 概述 中的图表介绍。本文仅以以下几个例子加以说明:

获取所有可用的 RPC 服务:http://localhost:8080/vulcan/shindig/rpc?method=system.listMethods, 返回结果如下所示,可以看到所有 Shindig 支持的服务。

清单 3. 所有可用的 RPC 服务

				
{"result":[ 
        "samplecontainer.update", 
        "gadgets.supportedFields", 
        "userprefs.create","http.head", 
        "http.post", 
        "activities.supportedFields", 
        "gadgets.defaultRenderType", 
        "activities.delete", 
        "appdata.update", 
        "messages.delete","http.delete", 
        "userprefs.update", 
        "activitystreams.delete", 
        "activities.get", 
        "gadgets.metadata", 
        "messages.modify", 
        "activitystreams.supportedFields", 
        "activities.create", 
        "messages.create", 
        "activitystreams.create", 
        "cache.invalidate", 
        "people.supportedFields", 
        "http.put", 
        "activities.update", 
        "activitystreams.get", 
        "userprefs.get", 
        "appdata.delete","http.get", 
        "gadgets.token", 
        "activitystreams.update", 
        "samplecontainer.create", 
        "appdata.get","messages.get", 
        "system.listMethods", 
        "gadgets.tokenSupportedFields", 
        "people.get", 
        "samplecontainer.get", 
        "appdata.create"
]} 
		

所有这些 RPC 的服务最终都动态绑定到 osapi,用户可以在客户端通过 osapi 的方法调用对应的服务,比如 osapi.gadgets.metadata(request).execute(callback)、osapi.people.get(request).execute(callback)、osapi.appdata.update(request).execute 等。所有这些既可以通过 osapi 来调用完成,也可以直接通过调用 RPC 来完成,最终在服务器端由对应的 Handler 来处理,比如 activity 相关的服务都可以在 ActivityHanlder 里面找到,gadgets 相关的都可以在 GadgetHanlder 里面找到。下面的 code 以 gadgets.metadata 为例加以说明:

清单 4. 使用 osapi 访问 metadata

				
var gadgetUrl = "http://localhost:8080/container/sample-pubsub-2-publisher.xml"; 
var request = { 
    'container': "default", 
    'ids': gadgetUrl, 
    'fields': [ 
        'iframeUrl', 
        'modulePrefs.*', 
        'needsTokenRefresh', 
        'userPrefs.*', 
        'views.preferredHeight', 
        'views.preferredWidth'
    ] 
}; 
osapi.gadgets.metadata(request).execute(function(response) { 
    if (response.error) { 
        console.debug("error when getting metadata!"); 
    } else { 
        for (var id in response) { 
            response[id]['url'] = id; // make sure url is set 
        } 
        var gadgetInfo = response[gadgetUrl]; 
        console.debug("iframeUrl is :",gadgetInfo.iframeUrl); 
        console.debug("required features are :",gadgetInfo.modulePrefs.features);
        console.debug("gadget title is :",gadgetInfo.modulePrefs.title); 
    } 
}) 
		

得到的结果为:

iframeUrl is: http://localhost:8080/gadgets/ifr? 
    url=http%3A%2F%2Flocalhost%3A8080%2Fcontainer%2Fsample-pubsub-2-publisher.xml
    &container=default&view=%25view%25&lang=%25lang%25& 
    country=%25country%25&debug=%25debug%25&nocache=%25nocache%25& 
    v=16b40aa73ad5c7cf1769dcdc4f4b4e7a 
required features are {"pubsub-2":{"required":true},"core":{"required":true}} 
gadget title is Sample PubSub Publisher

同样的结果也可以通过直接调用 RPC 获取,在浏览器中输入:http://localhost:8080/rpc?method=gadgets.metadata& container=default&ids=http://localhost:8080/container/sample-pubsub-2-publisher.xml& fields=iframeUrl,modulePrefs.features,modulePrefs.title,得到的结果如下:

{"result":{ 
    "http://localhost:8080/container/sample-pubsub-2-publisher.xml": 
    {"modulePrefs":{ 
        "title":"Sample PubSub Publisher", 
        "features":{"pubsub-2":{"required":true},"core":{"required":true}} 
    }, 
    "iframeUrl":"http://localhost:8080/gadgets/ifr?url= 
        http%3A%2F%2Flocalhost%3A8080%2Fcontainer%2Fsample-pubsub-2-publisher.xml 
        &container=default&view=%25view%25&lang=%25lang%25 
        &country=%25country%25&debug=%25debug%25&nocache=%25nocache%25 
        &v=16b40aa73ad5c7cf1769dcdc4f4b4e7a"
    } 
}}
       

通过 metadat 请求获取到了 Gadget 的 iframeUrl 信息,可以直接设置到 iframe 的 src,这也是新的 Gadget 容器的做法。为了与下面的 REST API 做对比,我们再举一个列出用户所有好友列表的请求:http://localhost:8080/rpc?method=people.get&userId=john.doe&[email protected]。

清单 5. 使用 rpc 的 API 访问用户的好友列表

				
{"result":{ 
    "totalResults":3,"filtered":false,"sorted":false, 
        "list":[ 
            {"name":{"givenName":"Jane","formatted":"Jane Doe","familyName":"Doe"}, 
                "id":"jane.doe"}, 
            {"name":{"givenName":"George","formatted":"George Doe","familyName":"Doe"},
                "id":"george.doe"}, 
            {"name":{"givenName":"Maija","formatted":"Maija Meikäläinen", 
                "familyName":"Meikäläinen"},"id":"maija.m"} 
        ], 
    "updatedSince":false 
    } 
} 	
	

REST 接口

Shindig 实现了四种类型的 REST 服务:People、Activity、Appdata 和 Group。完整的 URI 列表请参考 Shindig 概述 中的图表介绍。下面以获取用户好友列表为例,对应的 REST 请求为:http://localhost:8080/social/rest/people/john.doe/@friends。

清单 6. 使用 REST API 访问用户的好友列表

				
{"startIndex":0, 
    "totalResults":3, 
    "entry":[ 
        {"name":{"givenName":"Jane","formatted":"Jane Doe","familyName":"Doe"}, 
            "id":"jane.doe"}, 
        {"name":{"givenName":"George","formatted":"George Doe","familyName":"Doe"}, 
            "id":"george.doe"}, 
        {"name":{"givenName":"Maija","formatted":"Maija Meikäläinen", 
            "familyName":"Meikäläinen"},"id":"maija.m"} 
    ],"itemsPerPage":3 
} 
	

回页首

Shindig 所提供的特性介绍

Shindig 提供的特性基本都在 shindig-feature 这个项目里面,而另外一些扩展的特性在 shindig-extra 项目里面。根据 OpenSocial 规范里面核心 Gadget 的规范,在 Gadget 的世界里有两种上下文(context):一种是 Gadget 自己的上下文;一种是 Gadget 可以嵌入到其中的 Gadget 容器的上下文。shindig 默认使用 iframe 的形式来展现一个 Gadget,这样这两种上下文就是完全隔离的。外面容器的上下文称为 container,里面 Gadget 的上下文称为 gadget。这样 shindig 里面的特性都是基于这两种上下文来构成的,每个特性都有一个 feature.xml 来描述该特性的名称、所依赖的特性以及该特性用到的 JavaScript 文件。而针对不同的上下文也可能需要不同的 JavaScript 文件。

以 pubsub-2 为例,下面展示的就是 pubsub-2 这个特性所定义的 feature.xml。对于 container 来说需要 pubsub-2-router 这个文件实例化一个 openajaxhub,而对于 gadget 则需要 pubsub-2 来创建一个新的 hubclient。因此该特性针对两种不同的上下文定义了不同的 JavaScript 文件。

清单 7.Shindig 中的特性定义

				
<feature> 
    <name>pubsub-2</name> 
    <dependency>globals</dependency> 
    <dependency>org.openajax.hub-2.0.5</dependency> 
    <gadget> 
        <script src="pubsub-2.js"/> 
        <script src="taming.js"/> 
    </gadget> 
    <container> 
        <script src="pubsub-2-router.js"/> 
    </container> 
</feature> 
	

shindig 的特性可以分为以下几类:

  • 一类是展现一个 Gadget 肯定都会用到的特性,诸如 globals、shindig.auth、core、rpc。
    • globals 定义了一些全局变量,如 gadgets、shindig、osapi。
    • shindig.auth 提供了设置、获取、以及更新 security token 的方法;security token 是 shindig 的基本认证机制,里面包含了 viewer、owner、appid、domain、container、gadgetUrl 等信息。
    • core 特性提供了展现一个 Gadget 所需要的核心功能,包括 io 操作、用户设置等。
    • rpc 特性提供了整个 rpc 调用的实现。shindig 默认使用 iframe 来展现 Gadget,如果 Gadget 需要访问外部容器定义的某些方法,就需要通过 rpc 调用来完成。
  • 一类是 Gadget 容器用到的特性,诸如 shindig-container、container。
  • 一类是 OpenSocial 相关的一些特性,诸如 osapi、opensocial-data、opensocial-reference 等。
  • 一类是一些 util 特性,诸如 shindig.uri、shindig.xhrwrapper、xmlutil。
  • 一类是服务器端实现的特性,诸如 content-rewriter 和 security-token。
  • 一类是跟 Gadget 的用户界面比较相关的特性,诸如 settitle、tabs、views、minimessages、flash。
    • settitle 顾名思义就是设置 Gadget 的标题,使用这个特性可以在 Gadget 里面根据需要来动态的设置 Gadget 的标题。
    • tabs 特性支持在 Gadget 里面方便的创建多个 tab 和对应的内容。
    • views 特性提供了可以在多个 view 之间切换的功能。我们知道 Gadget 可以定义多个 view,使用 views 特性可以*的在多个 view 间切换。
    • minimessages 特性则提供了几种类型的提示消息,比如显示多长时间就自动消失的消息提示。
    • flash 特性则可以在 Gadget 里面嵌入 flash 文件。
  • 还有一类则是为了 Gadget 之间的通信所用到的特性,pubsub 和 pubsub-2。
  • 另外一个特性则是 oauthpopup,为 shindig 支持 OAuth 认证提供的一个特性。

本系列的下一篇文章会详细介绍 pubsub-2 和 OAuth 相关的特性。

回页首

结束语

本系列的第一篇文章介绍了如何写自己的第一个 Gadget 以及 OpenSocial 的基本概念,本文则着重介绍了 OpenSocial 的参考实现 Shindig 的基本架构、客户端和服务器端的流程以及基本的特性。

本文来源 互联网收集,文章内容系作者个人观点,不代表 本站 对观点赞同或支持。如需转载,请注明文章来源,如您发现有涉嫌抄袭侵权的内容,请联系本站核实处理。

© 版权声明

相关文章