使用 Eclipse 在 Google App Engine 上创建 mashup,第 3 部分: 使用 RESTful Web 服务

来源:developerWorks 中国 作者:Michael Galpin
  
使用社会网络可以更轻松地获取并聚合数据,从而创建富有革新精神的新 Web 应用程序。但是,仍然必须处理创建可伸缩 Web 应用程序的所有常见问题。现在,使用 Google App Engine (GAE) 也可以简化工作。使用 GAE,可以不必考虑管理应用服务器池的所有事务,而是集中精力创建优秀的 mashup。本文是共分三部分的系列文章 “使用 Eclipse 在 Google App Engine 上创建 mashup” 的最后一部分,在本文中,将利用并进一步增强在前两部分中构建的应用程序。我们将添加查看应用程序的其他用户及订阅其聚合提要的功能,然后通过将应用程序公开为可由其他 mashup 使用的 Web 服务完成 mashup 构建。

关于本系列

在本系列中,将了解如何开始使用 Google App Engine (GAE)。在 第 1 部分 中,了解了如何设置开发环境,以便可以开始创建运行在 GAE 上的应用程序。还了解了如何使用 Eclipse 简化应用程序的开发和调试。在 第 2 部分 中,通过添加一些 Ajax 特性增强了该应用程序。还了解了在部署到 GAE 后如何监视该应用程序。本文是第 3 部分,将通过为应用程序创建 RESTful Web 服务返回到生态系统,这样其他人就可以使用它创建自己的 mashup。

GAE 是创建 Web 应用程序的平台。使用它的最重要的先决条件是具备 Python 知识,因为要在 GAE 中使用 Python 作为编程语言(目前为 Python V2.5.2)。对于本系列,具备一些典型的 Web 开发技能将会有帮助(例如,HTML、JavaScript 和 CSS 知识)。要针对 App Engine 进行开发,需要下载 App Engine SDK(请参阅 参考资料)。在本系列中,还使用 Eclipse V3.3.2 以辅助 GAE 开发(请参阅 参考资料)。并且需要 PyDev 插件以将 Eclipse 转换为 Python IDE。





订阅

到目前为止,我们的应用程序 aggroGator 允许用户聚合(mash up)多项常见 Web 服务并创建这些服务的聚合提要。现在,为了让事情变得有趣一些,我们希望允许用户订阅其他用户的提要(其中每个用户的提要可能是提要本身的聚合)。例如,假定需要设置一个帐户以在 Twitter、last.fm 和 del.icio.us 中订阅自己的提要,这样其他朋友随后可以订阅 aggroGator 提要以查看这些服务中的所有活动。要处理这种情况,需要再一次重新审视数据模型。

建模

要启用订阅,需要允许一个用户(帐户)订阅其他帐户列表。我们可以采取的一种方法是向每个帐户中添加用户列表。每个用户将添加一个订阅。此操作的代码将类似清单 1。


清单 1. 带有 user 列表的 Account 模型
class Account(db.Model):
    user = db.UserProperty(required=True)
    subscriptions = db.ListProperty(Account)

这种方法有一些优点。检索帐户时,可以获得该帐户订阅的所有其他帐户。这是使用诸如 GAE 的 Bigtable 之类的非关系数据库的常见策略:把所有相关数据保存在一起并且无需担心标准化之类的事务。但是,这种方法有一个缺点。如果需要显示特定用户订阅了哪些人的提要该怎么办?这样做的惟一方法是检索所有 Account 模型,查看所有订阅,并查看给定用户是否位于列表中。另外,可以在每个 Account 模型中保存两张列表 — 一张用于 subscriptions,一张用于 subscribers。我们不会采取这种方法,而是使用更传统的多对多模型,如清单 2 所示。


清单 2. Subscribe 模型
class Subscribe(db.Model):
    subscriber = db.ReferenceProperty(Account, required=True, 
collection_name='subscriptions')
    subscribee = db.ReferenceProperty(Account, required=True, 
collection_name='subscribers')

正如您所见,这个模型类似于关系数据库中的连接表(join table)。只是因为 GAE 使用非关系数据库(Bigtable)并不意味着您不能利用与关系数据库结合使用的技术。现在数据模型已经就绪,让我们详细查看如何从最终用户的角度创建这些多对多关系。

订阅管理

我们的应用程序可以存储订阅,因此只需要一种方法能够让用户创建订阅。为此,需要为用户创建一个用于添加订阅的页面(参见清单 3)。


清单 3. 帐户页面
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/
    xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>Aggrogator Accounts</title>
    <link rel="stylesheet" href="/css/aggrogator.css" type="text/css" />
    <script type="text/javascript" src="/js/prototype.js"></script>
    <script type="text/javascript" src="/js/builder.js"></script>
    <script type="text/javascript" src="/js/effects.js"></script>
    <script type="text/javascript" src="/js/aggrogator.js"></script>
  </head>
  <body>

    <img id="spinner" alt="spinner" src="/gfx/spinner.gif" style="display: none; 
position: fixed;" />

    <div id="logout">
      {{ account.user.nickname }}
      <a href="{{ logout_url }}">Logout</a>
    </div>

    <div class="clearboth"></div>

    <ol>
      {% for acc in all_accounts %}
        <li>
          <a href="" onclick="subscribe('{{ acc.user.email }}'); return false;">
{{ acc.user.email }}</a>
        </li>
      {% endfor %}
    </ol>

  </body>
</html>

正如您所见,此页面只显示系统中的所有帐户。然后用户通过单击选择要订阅的帐户。您可以设想更复杂的界面。例如,如果存在大量用户,这样的界面将变得笨拙,因此基于搜索的系统将更好。或者使用一个系统,允许用户导入地址本或使用 OpenSocial 等应用程序中的 API 查找已经包含在应用程序中的现有朋友。以上模板需要一张用户列表,因此让我们快速查看为页面创建模型的控制器(参见清单 4)。


清单 4. 帐户页面控制器
#Accounts Module
class MainPage(webapp.RequestHandler):
    def get(self):
        # get the current user
        user = users.get_current_user()

        # is user an admin?
        admin = users.is_current_user_admin();

        # create user account if haven't already
        account = aggrogator.DB.getAccount(user)
        if account is None:
            account = aggrogator.Account(user=user)
            account.put()

        # create logout url
        logout_url = users.create_logout_url(self.request.uri)

        all_accounts = aggrogator.Account.all()

        template_values = {
            'account': account,
            'admin': admin,
            'logout_url': logout_url,
            'all_accounts': all_accounts,
            }
        path = os.path.join(os.path.dirname(__file__), 'accounts.html')
        self.response.out.write(template.render(path, template_values))

该控制器将获取准备显示在帐户页面中的所有数据。返回到清单 3 中的帐户页面,我们将看到在单击帐户时调用了 JavaScript。


清单 5. 订阅 JavaScript
function subscribe(email) {
  new Ajax.Request("/accounts/subscribe", {
    method: "post",
    parameters: {'email': email},
    onSuccess: alert('subscribed to ' + email)
  });
}

此 JavaScript 将再次使用 Prototype 库向服务器发出 Ajax 请求。调用 URL /accounts/subscribe。要把该 URL 映射到哪里?创建映射的代码位于新帐户模块的 main 函数中,如下所示。


清单 6. 帐户模块的 URL 映射
def main():
    app = webapp.WSGIApplication([
        ('/accounts/', MainPage),
        ('/accounts/subscribe', Subscribe),
        ], debug=True)
    util.run_wsgi_app(app)

if __name__ == '__main__':
    main()

正如您在 main 函数中看到的那样,对 /accounts/subscribe 的调用是由 Subscribe 控制器类处理的。清单 7 中显示了该类。


清单 7. Subscribe 控制器类
class Subscribe(webapp.RequestHandler):
    def post(self):
        # get the current user
        user = users.get_current_user()
        email = self.request.get('email')

        aggrogator.DB.create_subscription(user, email)

该控制器十分简单。它将获得当前用户(订阅者)和正在添加的订阅的电子邮件地址。然后将在以前使用的 DB 实用程序类中调用新方法。该类将处理所有与 Bigtable 相关的调用。下面显示了新 create_subscription 函数。


清单 8. create_subscription 的 DB 函数
class DB:
    @staticmethod
    def create_subscription(user, email):
        subscriber = DB.getAccount(user)
        subscribee = DB.getAccountForEmail(email)
        subscription = Subscribe.gql("WHERE subscriber = :1 AND subscribee = :2", 
subscriber, subscribee).get()
        if subscription is None:
            Subscribe(subscriber=subscriber, subscribee=subscribee).put()

    @classmethod
    def getAccountForEmail(cls, email):
        user = users.User(email)
        return cls.getAccount(user)

该函数首先查找 Account 模型以查找用户和订阅电子邮件。对于后者,它将使用新 getAccountForEmail 函数。这将使用 GAE 的用户的 API 根据电子邮件查找 User 对象,然后查询 Bigtable 查找帐户。找到两个帐户后,将查看订阅是否已经存在。如果不存在,则创建一个新订阅。

当然,有了订阅之后,需要在主应用程序中使用这些订阅。我们并不是仅显示当前用户的服务,而是需要显示聚合提要(还有用户的服务及来自其订阅的服务)。为此,需要对前几篇文章中开发的主模块的 GetUserServices 控制器进行微小更改,如下所示:


清单 9. 修改后的 GetUserServices 控制器
class GetUserServices(webapp.RequestHandler):
    def get(self):
        user = users.get_current_user()

        # get the user's services from the cache
        #userServices = aggrogator.Cache.getUserServices(user)
        userServices = aggrogator.Aggrogator.get_services(user.email())

        stats = memcache.get_stats()
        self.response.headers['content-type']  = 'application/json'
        self.response.out.write(simplejson.dumps({'stats': stats, 'userServices': 
userServices}))

在这段代码中,我们只调用了一个新库类:相应命名的 aggrogator 类,获得聚合服务而不仅仅是用户的服务。该库的代码如下所示:


清单 10. aggrogator 库:检索聚合服务
class aggrogator:
    @staticmethod
    def get_services(username):
        accounts = []
        
        primary = DB.getAccountForEmail(username)
        accounts.append(primary)

        for subscription in primary.subscriptions:
            accounts.append(subscription.subscribee)
        services = []
        for account in accounts:
            services.extend(Cache.getUserServices(account.user))   
        return services   

在这里,可以再次看到新 Subscribe 模型如何工作。在代码中,获得用户名的帐户(通过使用以前看到的 getAccountForEmail 函数),然后调用其订阅属性。在本例中,只使用此函数从缓存中获得所有服务。稍后,我们将看到这些用于创建聚合提要的服务。

现在几乎已经准备好测试新帐户页面。必须作出最后一项更改:需要将应用程序配置为向新帐户模块发送某些 URL 请求。为此,需要编辑 app.yaml 文件并添加一个新部分。


清单 11. 添加到 app.yaml 中的内容
- url: /accounts/.*
  script: accounts.py
  login: required

这只是文件的新部分。它将把带有 /accounts/ 的所有请求映射到帐户模块中。这部分代码应当会显示在先前使用的 catch-all 处理程序(url: /.*)之前,以便获得优先权。现在可以像以前一样使用 Eclipse 和 PyDev 并转到 http://localhost:8080/acounts/ 测试该应用程序。一定要创建多个帐户,这样测试会十分有趣。





aggroGator Web 服务

使用社会 Web 服务可以非常轻松地创建诸如 aggroGator 之类的有趣应用程序。GAE 允许我们创建此类同样具有很强可伸缩性的 mashup。因此,围绕 mashup 创建 API/Web 服务,以便其他人可以使用它创建自己的有趣 mashup 将十分有意义。这实现起来也非常简单。

对于我们的 Web 服务,首先把它制作成只读服务。该服务只为用户提供聚合提要(即在 aggroGator UI 中看到的相同内容)。使用简单的 REST 样式的 URL,例如 /api?username=my@email.address。这一次,将由下至上开始。要处理这样的 URL,需要再次向 app.yaml 文件中添加一个部分。


清单 12. 添加到 app.yaml 中的内容
- url: /api
  script: main.py

注意,仍然把 /api 请求发送给主模块。app.yaml 中为什么需要一个新映射?我们不需要对 aggroGator API 进行验证。这是在 app.yaml 中需要新规则的惟一原因。由于利用主模块,因此需要对它进行修改。


清单 13. 主模块的新规则
def main():
    app = webapp.WSGIApplication([
        ('/', MainPage),
        ('/addService', AddService),
        ('/getEntries', GetEntries),
        ('/api', AggroWebService),
        ('/getUserServices', GetUserServices),
        ], debug=True)
    util.run_wsgi_app(app)

if __name__ == '__main__':
    main()

对该函数所做的全部操作是向映射列表中添加一个条目。将把 /api 映射到 AggroWebService 控制器类中。该类如下所示:


清单 14. AggroWebService 控制器类
class AggroWebService(webapp.RequestHandler):
    def get(self):
        self.response.headers['content-type'] = 'text/xml'
        username = self.request.get('username')
        entries = aggrogator.Aggrogator.get_feed(username)
        str = u"""<?xml version="1.0" encoding="utf-8"?><entries>"""
        for entry in entries:
            str += entry.to_xml()
        str += "</entries>"
        self.response.out.write(str)

该服务首先检索 username 请求参数,然后使用以前看过的 aggroGator 库,但是使用不同的方法 get_feed 以获得聚合条目。该库函数的代码如下所示:


清单 15. aggroGator get_feed
class Aggrogator:
    @staticmethod
    def get_feed(username):
        services = Aggrogator.get_services(username)
        entries = []
        for svc_tuple in set((svc['service'], svc['username']) for svc in services):
            entries.extend(Cache.getEntries(*svc_tuple))
        entries.sort(key=operator.attrgetter('timestamp'), reverse=True)
        return entries
		

该 library 函数将使用在清单 10 中看到的 get_services 函数检索聚合服务,然后遍历服务。代码将使用一个集合以确保服务是惟一的(即如果用户已经订阅了都使用同一项服务的另外两个用户)。由于使用了集合,因此必须使用元组(tuple),因为只能使用不可修改的对象。最后,按时间戳降序排列所有条目(首先列出最新的条目)。

返回到清单 14,在拥有条目列表之后,使用某个简单的字符串连接来创建 XML 文档。对每个 Entry 实例使用 to_xml() 方法。这是一个新方法,如下所示:


清单 16. Entry 类
class Entry:
    def __init__(self, service=None, username=None, title=None, 
link=None, content=None, timestamp=None):
        self.service = service
        self.username = username
        self.title = title
        self.link = link
        self.content = content
        self.timestamp = timestamp
    def to_dict(self):
        return self.__dict__
    def to_xml(self):
        str = """<entry>
                    <service>%s</service>
                    <username>%s</username>
                    <title>%s</title>
                    <link>%s</link>
                    <content><![CDATA[%s]]></content>
                    <timestamp>%s</timestamp>
                </entry>"""
        return str % (self.service, self.username, self.title, self.link, self.content, 
self.timestamp) 

正如您所见,to_xml() 方法仅使用字符串模板和字符串替换来创建 XML 节点。返回到清单 14,在将 XML 文档创建为字符串后,设置内容类型的响应头部并将 XML 字符串发送回给请求者。这是我们需要做的全部操作,我们创建了一个可以供其他 mashup 使用的 Web 服务。





结束语

关于 Google App Engine 的 “使用 Eclipse 在 Google App Engine 上创建 mashup” 系列的第 3 部分到此结束。在本文中,添加了订阅和用于创建订阅的 UI。修改了使用订阅的现有应用程序,并且创建了 REST 样式的 Web 服务以允许其他 mashup 从 aggroGator 进行构建。在此之后我们还可以完成更多事务。可以把注释添加到 Entry 类中,添加允许用户向条目添加注释的 UI。可以提供订阅视图和个人视图。可以扩展 Web 服务,以便允许用户直接添加到提要中。由于使用了 Google App Engine,并且结合使用了 Eclipse 和 PyDev 等工具,完成所有这些工作变得更加容易。(责任编辑:A6)


时间:2008-09-27 09:21 来源:developerWorks 中国 作者:Michael Galpin 原文链接

好文,顶一下
(0)
0%
文章真差,踩一下
(0)
0%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量