2007 年 10 月 30 日
本文是四篇系列文章的第三篇,我们将在本文中开发一个简单的 blog 应用程序,这个程序接收 VoiceXML(VXML)输入并将数据保存到在线 blog 中。还要学习在创建 tweet(即 Twitter 消息)时如何使用这种语音 blog 技术。
blog 是另一个热门的领域,越来越多的人利用 blog 向公众表达自己的想法。为什么不通过 VoiceXML 用自己的声音与 blog 或 tweet 进行交互呢?在本文中,您将学习如何实现这种非常酷的功能以及:
- 从远程数据生成动态的 VoiceXML
- 将内容传递给 VXML
- 通过 VXML 向 blog 提交请求并接收报告
- 向 Twitter 提交状态更新
语音(和一般的音频)在网上越来越流行了。这方面的示例包括大量的网络音乐和网络广播。本系列讲解几种结合使用语音和 XML 开发应用程序的方式:
- 第 1 部分 — 支持语音的 RSS 阅读器
- 第 2 部分 — 支持语音的日历
- 第 3 部分 — 支持语音的 blog 和 Twitter 应用程序
- 第 4 部分 — 支持语音的 Yahoo 搜索应用程序
对于 blog 作者来说,最糟糕的事情莫过于无法访问自己的 blog,无法提交文章。但是,通过使用 VoiceXML,可以提供另一种与 blog 进行交互的方法。可以构建一个支持 VoiceXML 的应用程序,它允许用户通过电话访问 blog。
但糟糕的是,当前的 VoiceXML(VXML)平台能力有限,无法提交完整的 blog 文章;但是,可以使用 VoiceXML 向现有的 blog 提交一些标准化的消息。
为了实现这个功能,我们将构建一个 Java™ servlet,它将生成 VXML,用输入组装 blog 帖子,并将帖子发送到用户的 blog。目前,我们忽略 blog 选择、用户名和密码的问题,当我们看到 blog 接口时就会明白这么做的原因。
为了实现这个工作流,blog 接口 servlet 将:
- 从 blog 获得类别列表
- 生成一个 VXML 文件,它输出接收到的短语列表,然后输出接收到的类别列表
- VXML 使用输入方法将类别和短语信息提交给 servlet
- servlet 组装 blog 帖子并将它发送给 blog 以便提交
- 然后 VXML 输出一个 “已经完成” 消息,处理过程结束
在图 1 中可以看到基本结构。
在处理 VXML 之前,需要研究一下支持连接 blog 的 blog 接口。
有几种用于发布 blog 文章的解决方案,但是大多数 blog 都支持基于 MetaWeblog/MovableType 或 Blogger API 接口的 XML-RPC(XML Remote Procedure Call)标准。这两者非常相似,但是前者是专为解决后者的一些限制设计的,因此它更加实用。
在考虑建立 Web 帖子之前,需要设置一个能够向用户的 blog 发出 XML-RPC 调用的环境。在与大多数 blog 进行通信时,需要三段信息:
- blog 的 XML-RPC URL —— 对于 WordPress 这样的系统,通常由一个特殊的组件处理 XML-RPC 过程。例如,WordPress 提供了 xmlrpc.php 脚本。我们需要完整的 URL,例如 http://mcslp.com/xmlrpc.php。
- 用户名 —— blog 用户的用户名。
- 密码 —— blog 用户的密码。
对于所有 blog,还需要最后一段信息,blog ID。对于只包含一个 blog 的站点和系统(比如 WordPress),blog ID 总是 1。对于包含多个 blog 的站点和系统,需要 blog 的惟一 ID。MovableType、b2evolution 以及大型内容管理系统中包含的 blog 属于后一类。
显然,无法轻松地通过 VoiceXML 接口交换这一信息。但是,如果设置一个大型服务,那么可以提供一个使用数字的登录服务,让用户能够登录自己的帐户,然后装载他们的服务属性。
为了打开到 blog 的连接,我们围绕 XML-RPC 连接创建一个新的包装器类,它实现两个必需的功能:
- 获得类别的列表
- 通过主类的方法发布帖子
因此,主类实例只需设置所需的核心参数并创建一个 XML-RPC 对象的实例,我们将使用这个对象的实例与 blog 系统通信。
为了简化这个过程,我们将使用 Apache 提供的 XML-RPC 库。清单 1 给出 BlogPost
类的核心代码和创建这个类的实例的方法。
import java.util.*; import java.io.*; import java.net.URL; import org.apache.xmlrpc.client.XmlRpcClient; import org.apache.xmlrpc.client.XmlRpcClientConfigImpl; public class BlogPost { String username; String password; String blogid; String blogurl; XmlRpcClientConfigImpl xmlrpcConfig = new XmlRpcClientConfigImpl(); XmlRpcClient client = new XmlRpcClient(); public BlogPost(String user, String pass, String id, String url) { username = user; password = pass; blogid = id; blogurl = url; try { xmlrpcConfig.setServerURL(new URL(blogurl)); client.setConfig(xmlrpcConfig); } catch (Exception ex) { ex.printStackTrace(); System.out.println("ERROR: " + ex.getMessage()); } }
在 清单 1 中,可以看到如何接收输入,然后创建一个基本的 XmlRpcClient
实例,可以通过这个实例与 blog 的 XML-RPC 服务通信。
现在,为了创建 BlogPost
对象,使用清单 2 中的代码提供用户名、密码、URL 和 blog ID。
BlogPost bp = new BlogPost("user", "pass", "1", "http://myblog.com/xmlrpc.php");
在建立帖子之前,希望调用者能够根据 blog 中当前配置的类别设置一个类别。
基于 XML-RPC 的 weblog API 提供一个 getCategories
函数,这个函数返回指定的 blog 的类别列表。为了获得这一信息,需要在调用中提供 blog ID、用户名和密码。
返回值比较复杂。从数据结构的角度来说,它是一个散列值数组,其中的每个散列值由类别的相关信息组成,包括类别 ID、类别名称、类别的源 blog 引用 URL 和 RSS feed URL。因此,对于每个类别,会获得一个包含表 1 中的信息的散列值。
categoryId
3
htmlUrl
http://www.thewriting.biz/?cat=3
rssUrl
http://www.thewriting.biz?feed=rss2&cat=4
categoryName
Making Money
description
Making Money
其中一些信息是我们不需要的,我们只需要 categoryId
和 categoryName
。在建立 blog 帖子时,需要用 ID 设置类别。在应用程序的 VoiceXML 端提供类别名称,让用户能够按照名称选择类别。
为了使用这些信息,我们要发送请求、获取返回的信息并将类别 ID 和名称解析为一个 Java 散列表,可以在应用程序的 VoiceXML 部分使用这个散列表生成类别列表。清单 3 给出 BlogPost
对象的 GetCategories()
方法。
public Hashtable GetCategories() { Hashtable categories = new Hashtable(); Object[] cats = new Object[] {}; try { cats = (Object []) this.client.execute("metaWeblog.getCategories", new Object[] {this.blogid, this.username, this.password}); } catch (Exception ex) { ex.printStackTrace(); System.out.println("ERROR: " + ex.getMessage()); } for(int i = 0; i < cats.length; i++ ) { HashMap category = (HashMap)cats[i]; categories.put(category.get("categoryId"), category.get("categoryName")); } return categories; }
在 servlet 中,将使用产生的散列表输出类别列表。
在建立 blog 帖子时,要执行几个步骤,尤其是在希望追加帖子类别的情况下。对于某些 blog 系统,可以在一次提交中完成所有这些操作;但是,一些 blog 系统在提交帖子时无法接收 blog 类别。因此,更好的解决方案是执行以下步骤:
- 添加一个 blog 帖子,从而获得一个惟一的 blog 帖子 ID,但是将帖子标为不自动发布(这是
newPost
RPC)。 - 使用 blog 帖子 ID 为这个 blog 帖子设置类别(使用
setCategories
RPC)。 - 装载整个 blog 帖子的细节(使用
getPost
RPC)。 - 重新发布这个帖子,这一次将帖子标为可发布(使用
editPost
RPC)。
清单 4 给出向远程服务发送各种 XML-RPC 调用的操作过程。注意,与类别的情况一样,信息的格式也比较复杂。必须构造一个包含 blog 帖子信息的散列表。为了设置类别,还要创建另一个散列表,然后在提交类别信息时,以数组形式传递这个散列表(采用与 getCategories
的返回值相同的方式,即散列表的数组)。
public Boolean PostIt(String title, String description, Integer category) { Hashtable post = new Hashtable(); if (title != null) post.put("title", title); post.put("dateCreated", new Date()); post.put("description", description); Hashtable categories = new Hashtable(); categories.put("categoryId",category); String blogpostid = ""; Boolean result = Boolean.FALSE; try { blogpostid = (String) this.client.execute("metaWeblog.newPost", new Object[] {this.blogid, this.username, this.password, post, Boolean.FALSE}); result = (Boolean) this.client.execute("mt.setPostCategories", new Object[] {blogpostid, this.username, this.password, new Object[] {categories}}); HashMap postdetail = (HashMap) this.client.execute("metaWeblog.getPost", new Object[] {blogpostid, this.username, this.password, }); result = (Boolean) this.client.execute("metaWeblog.editPost", new Object[] {blogpostid, this.username, this.password, postdetail, Boolean.TRUE }); } catch (Exception ex) { ex.printStackTrace(); System.out.println("ERROR: " + ex.getMessage()); } return(result); }
实际的 blog 发布过程是对 PostIt()
函数的调用:
bp.PostIt("Title","Content",5);
既然 blog 发布机制和类已经就绪了,就只需要创建一个包装器 Java servlet,它输出所需的 VXML,然后接收输入以生成 blog 帖子。
在讨论生成 blog 帖子所需的 VXML 之前,需要研究一下接收用户输入所用的一些规则。
测试的基本语法格式是使用一种由圆括号和方括号组成的格式帮助指定所用的语法。正如前面提到的,还不能任意指定内容,但是可以接收一些基本短语并用在 blog 帖子中。
表 2 列出了最有用的操作符。
操作符
示例
描述
Disjunction
[happy sad]
可以接受其中的任意单词。
Concatenation
(very happy)
只接受这个完整的短语。
Optional Phrase
(?very happy)
问号(?)后面的单词是可选的,所以这个示例接受 “very happy” 和 “happy”。
Positive Closure
(+very happy)
加号(+)后面的单词必须出现至少一次,所以 “very happy” 和 “very very happy” 都是有效的。
Kleene Closure
(*very happy)
星号(*)后面的单词是可选的,但是也可以重复。所以 “happy”、“very happy” 和 “very very happy” 都是有效的。
显然,可以组合使用这些操作符,建立复杂的有效短语列表。
这个 blog 应用程序使用的语法见清单 5。
(eye am [happy { } sad { } traveling { } (on business){ } ] )
首先注意,这里用 “eye” 替代 “I”。大多数 VoiceXML 系统无法区分单词和单一字母,而且某些系统会拒绝接受少于两个字符的单词。因此,必须明确地使用单词的发音,然后使用一个定义的字段值捕捉真正需要的单词。
然后,只需提供可用选项的列表。根据 清单 5 中的语法,以下句子都是有效的输入:
- I am happy
- I am sad
- I am traveling
- I am on business
接下来,看看用来接收输入的 VXML 源代码。
VXML 源代码的核心部分见清单 6。
这个文件产生的基本交互序列与清单 7 相似。
Service: Welcome to the blogging service. S: Please state your current status to be used on your blog. User: I am happy S: Please state your current location U: London S: Your blog post will be: I am happy. Current location: London
然后,需要动态地输出最后一个字段 —— blog 中配置的类别列表。
为了产生类别列表,我们在 Java servlet 中生成 VXML 并插入一个 blog,它提示用户选择一个类别。可以使用 标记接受用户输入的值并分配一个数字类别 ID,在建立 blog 帖子时需要用这个 ID 指定类别。清单 8 给出输出类别列表的代码片段。
Hashtable cats = this.bp.GetCategories(); Enumeration keys = cats.keys(); while ( keys.hasMoreElements() ) { String key = (String)keys.nextElement(); String cat = (String)cats.get( key ); out.println("
现在,可以接收输入了。
VXML 中的 submit 标记将字段值作为标准 HTTP 参数传递给一个指定的远程应用程序。在 VXML 中,这个块放在 filled 部分中(见清单 10)。
为了接收输入并建立 blog 帖子,只需解析输入中的这些参数,然后将 blog 帖子的内容细节提交给 BlogPost
类实例的 PostIt()
方法,见清单 11。
public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { PrintWriter out = null; out = res.getWriter(); res.setContentType("text/xml"); this.bp.PostIt( req.getParameter("title"), req.getParameter("title") + ". I am currently located in " + req.getParameter("location"), Integer.parseInt(req.getParameter("category"))); printHeader(out); out.println(""); printFooter(out); }
现在,应该能够通过世界上的任何电话访问这个应用程序,说出您选择的句子、位置和类别,从而建立一个 blog 帖子。
现在,看看如何给 Twitter 增加语音功能。
Twitter 是一种近来受到许多关注的 Web 2.0 服务。(参见 参考资料 中 Twitter 的链接)。这种服务使用户能够及时了解朋友的动向,这不但可以在网上进行,而且可以通过手机或即时消息传递程序进行。您的朋友也可以了解您的动向。
这种服务的思路是,用户发送简短的文本(少于 140 个字符),让其他人了解他的位置以及马上要做的事情。Twitter 最初的意图是提供简单的状态消息,但是已经发展成一种 “快速 blog”,用户可以输入简单的想法并发送给朋友。
Twitter 的限制
Twitter 并不像 blog 那样提供类别,但是也允许添加位置信息。
那么,这又跟语音 blog 有什么关系?实际情况是各方面都有关系。但是,实时听写还没有实现,所以目前只能在一系列预先定义的短句子中进行选择。这对于 Twitter 非常合适。如果有一些常用的句子,那么可以预定义这些句子的语音;还可以提供一个 Web 界面,用户可以通过这个界面输入新的句子。
在 Twitter 中支持语音的过程与 blog 中的过程完全相同,但是 Twitter 不使用 XML-RPC。相反,需要在 HTTP POST 请求体中发送消息(即 “tweet”),见清单 12。
清单 12. 使用 HTTP POST 向 Twitter 提交消息
import java.net.*; import java.io.*; public class BlogPost { public BlogPost(String user, String pass, String status) { try{ Authenticator.setDefault(new PostAuthenticator(user, pass)); URL url = new URL ("http://twitter.com/statuses/update.xml"); URLConnection conn = url.openConnection(); conn.setDoInput (true); conn.setDoOutput (true); conn.setUseCaches (false); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); DataOutputStream printout = new DataOutputStream (conn.getOutputStream ()); String requestBody = "status=" + URLEncoder.encode (status); printout.writeBytes (requestBody); printout.flush (); printout.close (); DataInputStream input = new DataInputStream (conn.getInputStream ()); String str; while (null != ((str = input.readLine()))){ System.out.println (str); } input.close (); } catch (Exception e){ e.printStackTrace(); } } public static void main (String args[]){ BlogPost bp = new BlogPost("myusername", "mypassword", "Playing with voice blogging to the Twitter API!"); } }
稍后会解释 Authenticator
。在 清单 12 中,我们打开一个连接到 Twitter 的简单 HTTP 连接,并告诉系统我们将通过这个连接进行读写。
POST 请求的特征是,并不在 URL 中添加参数(比如 status
),而是作为请求体发送参数。
根据发送请求的目标是 update.xml 还是 update.json,会收到一个适当格式的响应;如果必要,可以检查这个响应,但是对于 blog 不需要这样做。
当然,您不希望任何人都能够更新您的 Twitter,所以需要提供身份验证机制。这就是这个类顶部的 Authenticator 的作用。
Authenticator 是一个抽象类,所以需要从它派生出子类,并实现 getPasswordAuthentication()
方法来存储用户名和密码(见清单 13)。
import java.net.Authenticator; import java.net.PasswordAuthentication; public class PostAuthenticator extends Authenticator { private String user; private String pass; public PostAuthenticator (String user, String pass){ this.user = user; this.pass = pass; } protected PasswordAuthentication getPasswordAuthentication(){ char[] passwd = new String(this.pass).toCharArray(); PasswordAuthentication auth = new PasswordAuthentication(this.user, passwd); return auth; } }
通过返回这个对象,可以将它设置为 Authenticator 类的默认值,当在 URLConnection 中需要执行身份验证时,Java servlet 会检查这些值。
最终结果是可以通过语音更新 Twitter。
本文介绍了如何构建一个 blog 解决方案,从而使用 VoiceXML 提供输入方法来建立 blog 帖子。但糟糕的是,听写功能还未得到广泛支持,所以还无法进行自由的语音输入。我们只能通过详细的语法规则选择简单的短语,但是这对于 Twitter 是很合适的。还可以声明自己的当前位置。
最终的应用程序是一个 Java servlet,它生成查询信息,对输入进行解析,然后提交帖子。
请务必阅读第 4 部分,即本系列中的最后一篇文章。在第 4 部分中,我们将开发一个应用程序,它接收 VoiceXML 输入,并通过 Yahoo Search API 执行基本 Web 搜索和 Yahoo 本地搜索。
描述
名字
大小
下载方法
第 3 部分的示例代码
x-voicexml3-blog.zip
5KB
HTTP
学习
- 您可以参阅本文在 developerWorks 全球网站上的 英文原文。
- 参阅本系列的其他部分:
- 在 Java Web 开发框架中创建 VoiceXML 页面,第 1 部分: 使用 Java servlet 和 JSP 生成 VoiceXML(Brett McLaughlin,developerWorks,2006 年 11 月):了解 Java servlet 如何轻松地支持 VoiceXML 应用程序。
- 在 Java Web 开发框架中创建 VoiceXML 页面,第 2 部分: 扩展 Java 驱动的 VoiceXML 应用程序(Brett McLaughlin,developerWorks,2006 年 12 月):学习如何使用 servlet 扩展单页面应用程序。
- VoiceXML 2.1 规范:了解 Voice Extensible Markup Language 平台实现的共有特性集。
- IBM XML 认证:了解如何成为 IBM 认证的 XML 和相关技术开发人员。
- XML 技术文档库:在 developerWorks XML 专区中可以找到大量技术文章和提示、教程、标准和 IBM Redbooks。
- developerWorks 技术活动和网络广播:随时关注技术的最新进展。
- 技术书店 浏览关于这些主题和其他技术主题的书籍。
获得产品和技术
- Twitter:Twitter 使用户能够及时了解朋友的动向,这不但可以在网上进行,而且可以通过手机或即时消息传递程序进行。
- Rome RSS/Atom syndication:下载这些用来解析、生成和发布 RSS 和 Atom 提要的开放源码 Java 工具和库。
- Apache XML-RPC Client:获得这个用来连接 XML-RPC 源的简化接口。
- Voxeo:在这里可以找到大量信息和 VoiceXML 应用程序的解决方案,可以通过传统方式、VoIP 和 Skype 进行访问。
- IBM 试用版软件:使用这些可以从 developerWorks 直接下载的试用版软件构建您的下一个开发项目。
讨论
- 参与论坛讨论。
- XML 专区论坛:参与各个以 XML 为中心的论坛。
- developerWorks XML 专区:分享您的想法:阅读本文之后,可以在这个论坛中发表自己的评论和想法。XML 专区的编辑主持这个论坛,欢迎您参与。
- 通过参与 developerWorks blog 加入 developerWorks 社区。
Martin Brown 成为一名职业作家已经有 8 年多的时间了。他是很多书籍和文章的作者,内容涉及很多主题。他的特长包括很多开发语言和平台 —— Perl、Python、Java、JavaScript、Basic、Pascal、Modula-2、C、C++、Rebol、Gawk、 Shellscript、Windows、Solaris、Linux、BeOS、Mac OS/X 等等 —— 还包括 Web 编程、系统管理和集成。他会定期为 ServerWatch.com、LinuxToday.com 和 IBM developerWorks 撰写文章,在 Computerworld、Apple Blog 以及其他站点都会定期更新自己的博客,同时还是 Microsoft 的主题专家(SME)。您可以通过他的网站 http://www.mcslp.com 与他联系。