集成 Blogger API:使用 GData 协议开发博客编辑器应用
1. 搭建 HTTP 代理
为了能让应用直接与外部服务交互,我们需要搭建一个 HTTP 代理。将其部署在 /HTTPProxy 路径下,此路径可任意命名,但客户端类在 sendRequest 方法里必须使用相同的值:
RequestBuilder requestBuilder = new RequestBuilder(
RequestBuilder.POST, GWT.getModuleBaseURL()+"/HTTPProxy" );
若要把这个 Servlet 部署到像 Tomcat 这样的完整 Java Servlet 容器中,就需要创建一个 web.xml 文件来描述该 Servlet:
<web-app>
<servlet>
<servlet-name>HTTPProxy</servlet-name>
<servlet-class>com.gwtapps.server.util.HTTPProxy</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HTTPProxy</servlet-name>
<url-pattern>/HTTPProxy</url-pattern>
</servlet-mapping>
</web-app>
2. 选择 API 协议
2.1 Blogger API 发展
Blogger 博客服务支持 API 访问已有较长历史,不过其 API 历经多次变更。2003 年 Google 收购了 Blogger 服务背后的 Pyra Labs 公司,2006 年 Google 开始将 Blogger 的 API 迁移至 Google Data API(GData)。
2.2 GData 协议优势
我们将运用 GData 协议来访问 Blogger。尽管在编写本文时旧协议仍可使用,但 Blogger 会逐步淘汰它。GData 协议不仅对与 Blogger 交互有用,Google 还通过该协议提供众多其他服务。而且,GData 基于 Atom 发布协议,此协议对更多 Web 服务也很有用。
2.3 Atom 发布协议与 GData 协议
多数博客服务起初采用基于 RPC 的 API,随着新功能的添加,API 变得愈发复杂,难以管理和学习。于是,一些博客服务和开发者联合创建了 Atom 发布协议,旨在为内容的联合和创作制定通用的发布标准。
- Atom 发布协议 :是一种 REST API,利用 HTTP 方法修改由 URL 标识的数据,并使用 Atom XML 模式描述数据。更多信息可查看 Atom 发布协议 。
- GData 协议 :基于 Atom 发布协议,提供查询和认证扩展。更多信息可查看 GData 协议 。
3. 定义 BloggerService 类
我们会在 BloggerService 类中借助 HTTP 代理与 Blogger 服务交互,并更新应用视图。该类将继承 BlogService 抽象基类:
public abstract class BlogService {
private BlogEditorView view;
public BlogService( BlogEditorView view ){
this.view = view;
}
public BlogEditorView getView(){
return view;
}
public abstract void signin();
}
public class BloggerService extends BlogService implements BlogViewListener{
public BloggerService( BlogEditorView view ){
super(view);
}
}
BlogService 抽象基类为添加到应用中的任何博客服务提供统一的基类,能让代码同时与多个服务交互,例如简单的登录任务。 BloggerService 类继承该接口,实现 signin 方法以及博客服务的其他所有方法。
4. 登录 Google 账户
使用 GData 协议的应用,登录是较为复杂的任务之一,但与其他认证机制相比,它相对简单。对于像博客编辑器这样的 Web 客户端应用,我们使用 AuthSub 认证接口。
4.1 认证流程
认证流程分为三个步骤:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(获取一次性令牌):::process
B --> C(交换会话令牌):::process
C --> D(使用会话令牌请求服务):::process
D --> E([结束]):::startend
- 获取一次性令牌 :将应用重定向到 AuthSub URL:
https://www.google.com/accounts/AuthSubRequest,此 URL 需包含以下参数:-
next:告知 AuthSub 在用户授权后应将用户重定向到的 URL。 -
scope:指定应用感兴趣的 Google 服务,对于 Blogger,该值为http://www.blogger.com/feeds。 -
session:确定返回的令牌是否可用于获取会话令牌,值为 1 或 0,本应用使用 1。
组合后的 URL 如下:
java private static final String URL_AUTH_SUB = "http://www.google.com/accounts/ AuthSubRequest?next="+GWT.getModuleBaseURL()+"index.html&scope=http%3A%2F%2Fwww. blogger.com/feeds&session=1";
-
- 交换会话令牌 :用户授权并返回一次性令牌后,应用使用该令牌向
https://www.google.com/accounts/AuthSubSessionToken发送请求,将令牌作为 HTTP 请求头中的Authorization字段发送:
Authorization: AuthSub token=”token_value”
AuthSub 接口返回包含会话令牌的 HTTP 响应,例如:
Token=DQAA...7DCTN Expiration=20061004T123456Z - 使用会话令牌请求服务 :在每个请求的 HTTP 头中添加会话令牌:
Authorization: AuthSub token=”session_token_value”
4.2 代码实现
public void signin(){
//check for a saved token
if( sessionToken == null ){
sessionToken = Cookies.getCookie("BloggerAuthSub");
}
//if we have a token get the list of blogs
if( sessionToken != null ){
makeRemoteCall( URL_GET_BLOG_LIST, new GetBlogListCallback() );
}
else{
//check for a token in the URL parameters
String params = BrowserLocation.getURLParameters();
if( params.length() != 0 ){
String tokenKey = "token=";
int tokenIndex = params.indexOf(tokenKey);
//request a session token using single-use token
if( tokenIndex != -1 ){
String token = params.substring( tokenIndex+tokenKey.length());
makeRemoteCall(
URL_AUTH_SUB_SESSION, token, new GetTokenCallback() );
return;
}
}
//we don’t have any tokens so we need to redirect to Google
BrowserLocation.setLocation( URL_AUTH_SUB );
}
}
signin 方法首先检查会话令牌是否保存在 cookie 中。若有令牌,继续请求博客列表;若没有,检查 URL 参数中是否有一次性令牌,若有则使用该令牌请求会话令牌;若都没有,则将用户重定向到 AuthSub 接口。
5. 获取账户的博客 XML 列表
用户登录并获得会话令牌后,就可以与 Blogger 服务交互并管理博客。首先要获取用户可管理的博客列表,通过向 http://www.blogger.com/feeds/default/blogs 发送 HTTP GET 请求来实现:
makeRemoteCall( URL_GET_BLOG_LIST, new GetBlogListCallback() );
GetBlogListCallback 类处理服务器响应:
private class GetBlogListCallback implements RequestCallback {
public void onError(Request request, Throwable exception){
GWT.log( "error", exception );
}
public void onResponseReceived( Request request, Response response ){
getView().getLoadingPanel().loadingEnd();
if( handleResponse( response ) ){
//parse the response as XML
Element document = XMLParser.parse(
response.getText() ).getDocumentElement();
//iterate over each entry in the XML
NodeList items = document.getElementsByTagName("entry");
for(int i=0;i<items.getLength();i++ ){
//extract the required data
Element item = (Element)items.item(i);
String title = getElementText( item, "title" );
String link="";
String postLink="";
String feedLink="";
//get the different kinds of links
NodeList links = item.getElementsByTagName("link");
for(int j=0;j<links.getLength();j++ ){
Element linkElement = (Element)links.item(j);
String rel = linkElement.getAttribute( "rel" );
if( rel.equals("alternate") )
link = linkElement.getAttribute("href");
else if( rel.equals("http://schemas.google.com/g/2005#post") )
postLink = linkElement.getAttribute("href");
else if( rel.equals("http://schemas.google.com/g/2005#feed") )
feedLink = linkElement.getAttribute("href");
}
//create a new Blog instance from the XML data
Blog blog = new Blog( title, link );
blogPostLinks.put( blog, postLink );
getView().addBlog( blog, BloggerService.this );
//request the blog entries for this blog
makeRemoteCall( feedLink, new GetBlogEntriesCallback( blog ));
}
}
}
}
该类解析服务器返回的 XML 数据,创建 Blog 实例,并保存博客的发布链接和订阅链接。同时,立即使用订阅链接请求博客的文章列表。
6. 获取每篇博客的文章 XML 列表
加载 Blogger 服务信息的最后一步是获取每篇博客的文章。在解析博客的 XML 描述并获取其订阅 URL 后,立即发起请求:
makeRemoteCall( feedLink, new GetBlogEntriesCallback( blog ) );
GetBlogEntriesCallback 类处理服务器响应:
private class GetBlogEntriesCallback implements RequestCallback{
private Blog blog;
GetBlogEntriesCallback( Blog blog ){
this.blog = blog;
}
public void onError(Request request, Throwable exception){
GWT.log( "error", exception );
}
public void onResponseReceived(Request request, Response response){
getView().getLoadingPanel().loadingEnd();
if( handleResponse( response ) ){
//parse the XML response
Element document = XMLParser.parse(
response.getText() ).getDocumentElement();
//iterate over each entry in the response
NodeList items = document.getElementsByTagName("entry");
for(int i=0;i<items.getLength();i++ ){
//create a new blog entry and add it to the view
BlogEntry entry = new BlogEntry( blog );
blog.addEntry( entry );
entryFromXml( entry, (Element)items.item(i));
getView().getBlogView( blog ).addEntryAtEnd( entry );
}
}
}
}
该类解析服务器返回的 XML 数据,为每个文章元素创建 BlogEntry 实例,并将其添加到视图中。 entryFromXml 方法用于将 XML 数据复制到 BlogEntry 实例:
private void entryFromXml( BlogEntry entry, Element entryElement ){
entry.setTitle( getElementText( entryElement, "title" ) );
entry.setContent( getElementText( entryElement, "content" ) );
String editLink = "";
NodeList links = entryElement.getElementsByTagName("link");
for(int j=0;j<links.getLength();j++ ){
Element linkElement = (Element)links.item(j);
String rel = linkElement.getAttribute( "rel" );
if( rel.equals("alternate") )
entry.setLink( linkElement.getAttribute("href") );
else if( rel.equals("edit") )
editLink = linkElement.getAttribute("href");
}
entryEditLinks.put( entry, editLink );
}
private static String getElementText( Element item, String value ){
String result = "";
NodeList itemList = item.getElementsByTagName(value);
if( itemList.getLength() > 0 && itemList.item(0).hasChildNodes()){
result = itemList.item(0).getFirstChild().getNodeValue();
}
return result;
}
7. 发送 XML 以创建和保存文章
视图允许用户在博客上创建新文章并修改现有文章。当用户保存文章时,视图会调用 BlogViewListener 接口的 onEntrySaved 方法, BloggerService 类实现了该接口并负责在服务器上执行相应操作。
7.1 操作步骤
操作步骤如下:
1. 确定文章是新文章还是现有文章:通过在哈希表中搜索文章的编辑 URL 来判断。
2. 选择合适的 URL:如果是新文章,使用博客的发布链接;如果是现有文章,使用编辑链接。
3. 发送 HTTP 请求:使用 HTTPProxyRequestBuilder 类发送请求,并设置请求头和请求体。
7.2 代码实现
public void onEntrySaved( BlogEntry entry ){
getView().getSavingPanel().loadingBegin();
//create the request object
HTTPProxyRequestBuilder builder;
String editLink = (String)entryEditLinks.get( entry );
if( editLink == null ){
String postLink = (String)blogPostLinks.get( entry.getBlog() );
builder = new HTTPProxyRequestBuilder( RequestBuilder.POST, postLink );
}
else{
builder = new HTTPProxyRequestBuilder(RequestBuilder.POST, editLink );
builder.setHeader("X-HTTP-Method-Override", "PUT");
}
//set authorization to our token and set the content type to XML
builder.setHeader("Authorization", "AuthSub token=\""+sessionToken+"\"");
builder.setHeader("Content-type", "application/atom+xml");
//send the request using the entry XML as the body
try {
builder.sendRequest(
entryToXml( entry ), new PostEntryCallback( entry ) );
}
catch (RequestException e){ GWT.log( "error", e); }
}
7.3 注意事项
- 由于 Safari 浏览器的限制,我们使用
X-HTTP-Method-Override头来强制使用PUT方法。 -
entryToXml方法用于将BlogEntry实例转换为 XML 字符串:
private String entryToXml( BlogEntry entry ){
Document document = XMLParser.createDocument();
Element entryElement = document.createElement("entry");
Element titleElement = document.createElement("title");
Element contentElement = document.createElement("content");
Text titleText = document.createTextNode( entry.getTitle());
Text contentText = document.createTextNode( entry.getContent() );
document.appendChild(entryElement);
entryElement.setAttribute("xmlns","http://www.w3.org/2005/Atom");
entryElement.appendChild(titleElement);
entryElement.appendChild(contentElement);
titleElement.appendChild(titleText);
contentElement.appendChild(contentText);
contentElement.setAttribute("type","xhtml");
return document.toString();
}
7.4 处理响应
PostEntryCallback 类处理服务器的响应:
private class PostEntryCallback implements RequestCallback{
BlogEntry entry;
PostEntryCallback( BlogEntry entry ){
this.entry = entry;
}
public void onError(Request request, Throwable exception){
GWT.log( "error", exception );
}
public void onResponseReceived( Request request, Response response ){
getView().getSavingPanel().loadingEnd();
if( handleResponse( response ) ){
entryFromXml( entry, response.getText() );
getView().getBlogView( entry.getBlog() )
.getEntryView( entry ).update();
}
}
}
该类将服务器返回的更新后的文章数据复制回 BlogEntry 实例,并通知视图更新显示。
8. 发送删除文章的请求
我们还需要实现 REST 方法中的 DELETE 方法,用于删除文章。 BloggerService 类实现了 BlogViewListener 接口的 onEntryDeleted 方法:
8.1 操作步骤
操作步骤如下:
1. 获取文章的编辑链接:从哈希表中获取文章的编辑链接。
2. 创建 HTTP 请求:使用 HTTPProxyRequestBuilder 类创建请求,并设置请求头。
3. 发送请求:使用 sendRequest 方法发送请求,并处理响应。
8.2 代码实现
public void onEntryDeleted( BlogEntry entry ){
//create the request
HTTPProxyRequestBuilder builder;
String editLink = (String)entryEditLinks.get( entry );
if( editLink != null ){
builder = new HTTPProxyRequestBuilder( RequestBuilder.POST, editLink );
//set the authorization, content type, and method override headers
builder.setHeader("X-HTTP-Method-Override", "DELETE");
builder.setHeader(
"Authorization", "AuthSub token=\""+sessionToken+"\"");
builder.setHeader("Content-type", "application/atom+xml");
//send the request
try {
builder.sendRequest(entryToXml( entry ), new DeleteEntryCallback());
}
catch (RequestException e){ GWT.log( "error", e); }
}
}
8.3 处理响应
DeleteEntryCallback 类处理服务器的响应:
private class DeleteEntryCallback implements RequestCallback {
public void onError(Request request, Throwable exception){
GWT.log( "error", exception );
}
public void onResponseReceived(Request request, Response response){
handleResponse( response );
}
}
该类简单地处理响应,确保删除操作成功。需要注意的是,这里没有向用户验证文章即将被删除,因为视图层已经处理了用户确认的逻辑。
总结
通过以上步骤,我们实现了一个完整的博客编辑器应用,该应用可以与 Blogger 服务进行交互,包括登录、获取博客列表、获取文章列表、创建和保存文章以及删除文章等功能。以下是整个应用的操作流程总结:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(登录 Google 账户):::process
B --> C(获取博客列表):::process
C --> D(获取文章列表):::process
D --> E{操作选择}:::decision
E -->|创建文章| F(发送 XML 创建文章):::process
E -->|保存文章| G(发送 XML 保存文章):::process
E -->|删除文章| H(发送删除请求):::process
F --> I([结束]):::startend
G --> I
H --> I
整个应用的核心在于使用 GData 协议与 Blogger 服务进行交互,通过 HTTP 代理解决跨域问题,并使用 AuthSub 接口进行认证。在开发过程中,我们需要注意不同浏览器的兼容性问题,例如 Safari 浏览器对 PUT 和 DELETE 方法的支持。同时,我们还需要处理服务器返回的响应,确保数据的一致性和准确性。通过合理的设计和实现,我们可以开发出一个功能强大、用户友好的博客编辑器应用。
超级会员免费看
948

被折叠的 条评论
为什么被折叠?



