使用 Web 服务和 eBay SDK 开发应用程序,第 1 部分: 构建 eBay 搜索引擎
2009-12-09 00:00:00 来源:WEB开发网关于本教程
eBay 的清单的百分之四十是由使用 eBay 的 Web 服务的应用程序来驱动的。这些清单中的一半是来自于第三方开发人员所编写的应用程序。eBay 每个月处理超过十亿个来自它的 Web 服务 API 的请求。这就意味着两点:许多人通过供应商提供的应用程序来使用 eBay,并且这些应用程序可能做了较多的缓存。
对于 eBay 来说,人们开始使用它们的 Web 服务 API 开发应用程序非常重要,这是因为,它们在通过帮助人们进行虚拟的清仓拍卖来赚钱的同时,还通过帮助零售商将其整个业务都搬到网上来赚取真正的钱。这意味着可以通过编写应用程序帮助 eBay 将业务搬到网上来赚钱。并且,如同一个良好的合作伙伴,eBay 正努力向您这样的开发人员提供编写应用程序所需的全部,这些应用程序将为您和 eBay 赚钱。如果您不喜欢唯利是图的方面,您可能是 Richard Dawkins 的追随者,则请从这个角度考虑该问题:eBay 是一种发展策略,用于将您车库中的垃圾从它所在的地方(和您在一起但是未增值)移到它所希望的地方(某个增值的地方)。这种资本的分配是有助于激发您优化这种转移的要素。
开始之前
本教程有如下假定:
您已经加入了 eBay 开发人员计划 (eBay Developers Program)。
您已经收到了您的 devId、appId 和 certId。
您已经下载并安装了 eBay Java SDK。
您已经在沙箱 (Sandbox) 中创建了用户并拥有了用户 ID。
您已经生成了授权令牌。
您可以成功地执行 eBay SDK 示例文件夹 (*whew!*) 中的 ApiCallsDemo 应用程序。
遗憾地是,由于篇幅方面的考虑,我不能详细地描述这些步骤。
引言
SDK 概述
eBay 创建了 Web 服务 API 并通过 SOAP 将其公开,以便使您可以使用任何支持 SOAP 的编程语言来编写 eBay 应用程序。直接使用 SOAP 可能是困难的,所以 eBay 在 SOAP 层之上创建了 Java SDK。该 SDK 提取直接处理 SOAP 的细节。该 SDK 包括很多类,但是它们巧妙地分成了三个主要的类别:
内核类 (Kernel class) 距离 SOAP 层最近,并且在某些情况下,作为其他类别中的类的构件。
调用类 (Call class) 是 com.ebay.sdk.call 包的一部分,表示特定的 eBay 服务的调用。
类型类 (Type class) 在 com.ebay.soap.eBLBaseComponents 包中,表示用于作为调用的参数或返回值的数据类型。
内核类
内核类在 com.ebay.sdk 包中。该类别包括 14 个类;其中的 3 个是异常,该示例只直接使用了 2 个。最重要的两个类是 ApiContext 和 ApiCredential。ApiContext 定义了调用任何 API 的上下文。所有的调用类都将 ApiContext 作为它们的构造器的参数。ApiContext 还保存了对 ApiCredential 的引用,它用于授权调用。如果凭证设置不正确,调用将会失败。
com.ebay.sdk 包还保存了让您控制应用程序的日志记录(缺省情况下,应用程序记录到 Axis 的日志)、定义异常的调用重试策略以及使用其他的普通应用程序行为的类。ApiCall 同样也是一个内核类,但是它主要用来作为接下来所描述的所有调用类的超类。然而,如果您愿意,您可以通过调用 execute 或者 executeByApiName 方法直接使用 ApiCall 类。
调用类
这些类包装了 SOAP API 调用。使用的模式如下:
实例化调用类,在构造函数中传递 ApiContext。
实例化并填充该调用所需要的所有参数。
调用执行方法并捕获所有的返回值。
执行方法最终委托给 ApiCall 类中的某个执行方法。方法的实际名称可能会有不同,这取决于调用类。例如,AddItemCall 用于列出条目;执行方法采用 ItemType 作为参数并返回 FeesType。所以,列出条目使用的模式如下:
ItemType item = new ItemType();
// populate item with all sorts of relevant information (omitted)
AddItemCall call = new AddItemCall(apiContext);
// assume we have an instance of the ApiContext
FeesType fees = call.addItem(item);
类型类
类型类是由 eBay Web 服务 Web 服务描述语言(Web Services Description Language,WSDL)所描述的数据类型的 Java 表示,并且不包含真正的行为。AbstractRequestType 和 AbstractResponseType 是这个包中的大部分类的超类。
引导 eBay 搜索
在 eBay 中搜索如何工作
执行 eBay 搜索的最简单的方法是提供一个查询字符串。使用过搜索引擎的任何人都应该很熟悉这种格式。查询由一个或多个以空格分隔的关键字所组成。标点符号用于提供查询的元信息。所有清单的标题和子标题按条件计算;条目描述没有用于这个计算中。您可以在 eBay 帮助页上找到对该查询语言的完整描述。
eBay 还具有高级的查询系统,允许您指定除了查询字符串以外的条件。例如,您可以将查询限制在某个类别,并且您可以基于条件(比如价格、清单类型和销售商)来筛选结果。
用例
您将在本文中开发的案例研究是一个接受文本字符串、查询 eBay 并返回结果的应用程序。下面是一个示例:
fetch: id title description query: godot +blooper category: 377
这一行包含了三个指令:fetch、query 和 category。您通过在文本的后面加上冒号来指示一条指令。图 1 列出了该语法。
图 1. 查询语法
QUERY | -> | ( FETCH_CLAUSE QUERY_CLAUSE CATEGORY_CLAUSE )+ |
DESCRIPTOR | -> | [a-zA-Z]+: |
FETCH_CLAUSE | -> | 'fetch:' COLUMN+ | <nil> |
QUERY_CLAUSE | -> | 'query:' QUERY_STRING |
CATEGORY_CLAUSE | -> | 'category:' CATEGORY | <nil> |
COLUMN | -> | 'id' | 'title' | 'subtitle' | 'description' | 'timeleft' | 'currentprice' |
QUERY_STRING | -> | [^DESCRIPTOR] |
CATEGORY | -> | [0-9]+ |
这是不严格的扩展巴科斯范式(Extended Backus-Naur Form,EBNF)语法。所有字母都大写的词是非终结符,而在右面的小写字母的词是终结符。
下面是一些有效查询的示例:
fetch: id title currentprice query: godot category: 377 这将提取在类别 377 中标题或者子标题中包含词 godot 的所有条目的 ID、标题和最高报价。fetch: title category: 377 这将提取在类别 377 中所有条目的标题。category: 377 query: godot fetch: id title currentprice 这与第一个查询是一样的——子句的顺序没有关系。
如果您没有指定 fetch 子句,搜索缺省使用 ID 和标题。如果您没有指定类别,搜索缺省使用所有的类别。唯一必需的子句是 query。因此,最简单的查询就是 query: <keyword>。
编写代码
驱动程序
该应用程序是从控制台驱动的,但是它是以这种方式设计的,即该控制台仅仅是表示层。这种逻辑可以像在 Web 应用程序或 GUI 应用程序中那样简单地使用,或者由数据库驱动。请参见 清单 1。
清单 1. Main.java
package org.thinkpig.ebay.search;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("eBayQueryTron v.001");
System.out.println("-------------------");
while(true) {
String input = getInput();
if (input.startsWith("quit")) { return; }
AbstractQuery query = QueryParser.parse(input);
System.out.println("Executing query --> " + query);
QueryResult result = QueryExecutor.executeQuery(query);
displayResult(result);
}
}
private static String getInput() throws IOException {
System.out.print("\n> ");
BufferedReader in =
new BufferedReader(new InputStreamReader(System.in));
return in.readLine();
}
private static void displayResult(QueryResult _result) {
List items = _result.getResultItems();
for(int i = 0; i < items.size(); ++i) {
displayItem((QueryResultItem)items.get(i));
}
}
private static void displayItem(QueryResultItem _item) {
for(int i = 0; i < _item.getNumberOfColumns(); ++i) {
System.out.println(_item.getValue(i));
}
}
}
这个代码中没有什么值得注意的。只要知道它是应用程序的入口点并作为主代码的驱动程序就足够了。它将应用程序的逻辑粘合在一起——QueryParser 和 QueryExecutor。它还显示了 QueryExecutor 的结果。
QueryParser 和 AbstractQuery
AbstractQuery 是表示在图 1 中所描述的 BNF 的数据类型。AbstractQuery 类(请参阅 清单 2)映射到 QUERY 规则,并且它具有它所支持的三个子句的属性。描述符就是类中的属性;描述符后面的规则映射到 Java 数据类型。例如,QUERY_CLAUSE 是由 QUERY_STRING 后面的查询描述符 (query:) 所构成。在 Java 语言中这将作为 private String queryString 实现。COLUMN_CLAUSE 由一个或多个 COLUMN 所组成,所以它表示为 List。
清单 2. AbstractQuery.java
package org.thinkpig.ebay.search;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class AbstractQuery {
public static final String NO_CATEGORY = "NO CATEGORY";
private String category = NO_CATEGORY;
private String eBayQuery = "";
private List columns = new ArrayList();
private boolean useDefaults = true;
public AbstractQuery() {
this.columns.add("id");
this.columns.add("title");
}
public void setCategory(String _category) {
if (_category == null) {
throw new IllegalArgumentException("Category must
not be null.");
}
this.category = _category;
}
public String getCategory() {
return this.category;
}
public void addColumn(String _column) {
if (this.useDefaults) {
this.columns.clear();
this.useDefaults = false;
}
this.columns.add(_column);
}
public List getColumns() {
return Collections.unmodifiableList(this.columns);
}
public String toString() {
return "fetch: " + this.columns + " query: " +
this.eBayQuery + " category: " + this.category;
}
public void setEBayQuery(String _query) {
this.eBayQuery = _query;
}
public String getEBayQuery() {
return this.eBayQuery;
}
}
QueryParser 是将输入字符串变成 AbstractQuery 的对象(请参见清单 3)。
清单 3. QueryParser.java
package org.thinkpig.ebay.search;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class QueryParser {
private static String currentToken = null;
private static List tokens;
private static String END = "!END"; // not a member of the
// token alphabet
public static AbstractQuery parse(String _queryString) {
AbstractQuery query = new AbstractQuery();
if (_queryString == null || _queryString.length() == 0) {
throw new ParseException("Query string must be non-empty.");
}
// if we don't trim first, split will give us a "" token
_queryString = _queryString.trim();
tokens = new ArrayList(Arrays.asList(_queryString.split("\\s+")));
nextToken(); // prime the pump
parse(query);
return query;
}
private static void parse(AbstractQuery _query) {
while(!currentToken.equals(END)) {
if(currentToken.equals("query:")) {
nextToken();
parseQueryClause( _query);
continue;
}
if(currentToken.equals("category:")) {
nextToken();
parseCategoryClause(_query);
continue;
}
if(currentToken.equals("fetch:")) {
nextToken();
parseFetchClause(_query);
continue;
}
throw new ParseException("Unknown descriptor: " + currentToken);
}
}
private static void parseQueryClause(AbstractQuery _query) {
StringBuffer buf = new StringBuffer();
while(!currentToken.equals(END)) {
if (currentToken.endsWith(":")) { break; }
buf.append(currentToken);
buf.append(" ");
nextToken();
}
_query.setEBayQuery(buf.toString().trim());
}
private static void parseFetchClause(AbstractQuery _query) {
while(!currentToken.equals(END)) {
if (currentToken.endsWith(":")) { return; }
_query.addColumn(currentToken);
nextToken();
}
}
private static void parseCategoryClause(AbstractQuery _query) {
_query.setCategory(currentToken);
nextToken();
}
private static void nextToken() throws ParseException {
try {
currentToken = (String)tokens.remove(0);
} catch (IndexOutOfBoundsException _ioobe) {
currentToken = END;
}
}
private static String peek() {
if(tokens.size() > 0) {
return (String)tokens.get(0);
}
return END;
}
}
QueryParser 是一个简单的递归下降解析器 (recursive descent parser) 的示例。它以类似于 AbstractQuery 的方式表示了语法。对于该语法中的每个转移规则,您都有一个方法。parse(AbstractQuery _query) 方法对应于 QUERY -> CLAUSE+ 规则。非正式地,该方法表示,“只要还有令牌,就作为查询子句、类别子句或者提取子句进行解析”。QUERY_CLAUSE 规则是后面跟随 QUERY_STRING 的查询描述符。QUERY_STRING 可以是除描述符以外的任何事物。parseQueryClause 方法将它找到的所有令牌附加到 queryString,直到找到描述符令牌为止。这个逻辑类似于 parseFetchClause 和 parseCategoryClause 方法。
引导 eBay 查询
QueryExecutor 和 eBayApi
现在,您已经拥有了希望用作 eBay 查询基础的 AbstractQuery。QueryExecutor 接受 AbstractQuery,将其转化为 eBay 格式,并把它发送给 Web 服务(请参见清单 4)。
清单 4. QueryExecutor.java
package org.thinkpig.ebay.search;
import java.util.Iterator;
import com.ebay.sdk.ApiException;
import com.ebay.sdk.SdkException;
import com.ebay.sdk.call.GetSearchResultsCall;
import com.ebay.soap.eBLBaseComponents.GetSearchResultsRequestType;
import com.ebay.soap.eBLBaseComponents.ItemType;
import com.ebay.soap.eBLBaseComponents.SearchResultItemType;
public class QueryExecutor {
private static final EbayApi api = new EbayApi();
public static QueryResult executeQuery(AbstractQuery _query) throws
Exception {
GetSearchResultsCall call = convertQuery(_query);
SearchResultItemType[] results = call.getSearchResults();
QueryResult queryResult = convertResults(_query, results);
return queryResult;
}
private static GetSearchResultsCall convertQuery(AbstractQuery _query) {
GetSearchResultsCall call =
new GetSearchResultsCall(api.getApiContext());
call.setQuery(_query.getEBayQuery());
if (!_query.getCategory().equals(AbstractQuery.NO_CATEGORY)){
call.setCategoryID(_query.getCategory());
}
return call;
}
private static QueryResult convertResults(AbstractQuery _query,
SearchResultItemType [] _results) throws SdkException {
QueryResult result = new QueryResult();
if (_results == null) {
return result;
}
for (int i = 0; i < _results.length; i++) {
SearchResultItemType type = _results[i];
result.addItem(convertItem(_query, type));
}
return result;
}
private static QueryResultItem convertItem(AbstractQuery _query,
SearchResultItemType _resultType) throws SdkException {
QueryResultItem item = new QueryResultItem();
Iterator i = _query.getColumns().iterator();
while(i.hasNext()) {
String column = (String)i.next();
String value = findValue(column, _resultType);
item.addColumn(column, value);
}
return item;
}
private static String findValue(String _column,
SearchResultItemType _resultType) throws SdkException {
ItemType item = _resultType.getItem();
if (_column.equalsIgnoreCase("id")) { return
item.getItemID().getValue().trim(); }
if (_column.equalsIgnoreCase("title")) { return
item.getTitle().trim(); }
if (_column.equalsIgnoreCase("subtitle")) { return
item.getSubTitle().trim(); }
if (_column.equalsIgnoreCase("description")) {
return item.getDescription().trim();
}
if (_column.equalsIgnoreCase("timeleft")) {
return item.getTimeLeft() ==
null ? "Unknown" : item.getTimeLeft().toString();
}
if (_column.equalsIgnoreCase("currentprice")) {
return
item.getSellingStatus().getCurrentPrice().toString().trim();
}
throw new SdkException("No SDK mapping for column: " + _column);
}
}
QueryExecutor 做了三件事:它创建 GetSearchResultsCall 并使用来自 AbstractQuery 的数据填充它;它对调用对象调用 getSearchResults() 方法,这将调用 eBay Web 服务;它将结果转换为 QueryResult,以便特定于 eBay 的类型不会泄漏到应用程序的其余部分。GetSearchResultsCall 是 eBay API 的一部分,与 SearchResultItemType 一样。它们是到目前为止应用程序所依赖的 eBay SDK 的唯一部分。您严格地将应用程序与 eBay SDK 隔离开来,因为这样做有助于保护您的逻辑不受 eBay 可能对其 SDK 所做的更改的影响。eBay 善于维护向后的兼容性,但是 API 正迅速地发展。当您更改代码以便利用新的功能时,您肯定希望将改变保持在最低程度。
请注意 findValue 方法的底部,如果条目没有您感兴趣的列的映射,它将抛出 SdkException。eBay Java API 有两个主要的异常:SdkException 和 ApiException。SdkException 用于指示 SDK 内部的异常条件,而 ApiException 指示调用 Web 服务的问题。在本例中,您将抛出 SdkException,因为您不能将查询列映射到条目的属性。如果您试图查询一个不存在的类别 ID,那么您将得到 ApiException。
EbayApi 类封装了获得 ApiContext 并使用适当的凭证将其填充的流程(请参见清单 5)。当您使用这个代码时,请确保将 USER_AUTH_TOKEN 设置为测试用户的令牌。另外,还请注意,serverURL 指向沙箱。在生产应用程序中,这个 URL 将指向生产服务器。
清单 5. EbayApi.java
package org.thinkpig.ebay.search;
import com.ebay.sdk.ApiContext;
import com.ebay.sdk.ApiCredential;
public class EbayApi {
private static final String USER_AUTH_TOKEN =
"<<Insert your token here.>>";
private static final String serverURL =
"https://api.sandbox.ebay.com/wsapi";
private ApiContext apiContext;
public EbayApi() {
this.apiContext = new ApiContext();
ApiCredential cred = apiContext.getApiCredential();
cred.seteBayToken(USER_AUTH_TOKEN);
apiContext.setApiServerUrl(serverURL);
}
public ApiContext getApiContext() {
return this.apiContext;
}
}
对于这个示例,您需要做的只是实例化一个新的 ApiContext,将它的 ApiCredential 的 eBayToken 属性设置为您的令牌,并设置 ApiServerUrl。然而,您在这里还可以进行任何其他上下文级的配置(例如日志记录)。请注意,ApiContext 的属性并不是不变的,所以需要引用 ApiContext 的任何代码(而且这是许多代码)可以更改它的配置。实际上它可能并不需要您担心,但是它是值得了解的。
获得查询结果
QueryResult 和 QueryResultItem
您已经见到了所有的好东西。唯一剩下的就是 QueryResult(清单 6)和 QueryResultItem(清单 7)。后者表示单独的搜索结果,而前者将它们全部捆绑成可以传送的一个对象。这些类将搜索结果的概念抽象为表示层在不必了解 eBay SDK 的细节的情况下可以解构的某个对象。
清单 6. QueryResult.java
package org.thinkpig.ebay.search;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class QueryResult {
private final List results = new ArrayList();
public void addItem(QueryResultItem _item) {
results.add(_item);
}
public List getResultItems() {
return Collections.unmodifiableList(results);
}
}
请注意,您不只是返回对结果的引用。您还需要保持该结果不变,所以 getter 将其包装在不可修改的列表包装程序中。
清单 7. QueryResultItem.java
package org.thinkpig.ebay.search;
import java.util.ArrayList;
import java.util.List;
public class QueryResultItem {
private final List columns = new ArrayList();
private final List values = new ArrayList();
public void addColumn(String _columnName, String _columnValue) {
// we use this instead of a map to retain order
columns.add(_columnName);
values.add(_columnValue);
}
public int getNumberOfColumns() {
return columns.size();
}
public String getValue(int _index) {
return (String)values.get(_index);
}
}
结束语
本教程演示了 eBay SDK 的使用相当简单。主要的问题就是找到正确的调用类,使用适当的参数设置它的属性,并调用它。您不必与 SOAP 部分搅在一起。然而,这似乎意味着使用 SDK 构建 eBay 应用程序非常简单。如果真是这样,SDK 就不会附带 958 页的 PDF 文档;开发人员站点就不会有论坛、FAQ 、知识库、白皮书、示例代码以及采用流式视频的应用程序开发课程。eBay SDK 有点像 XML——它容易学习,但是又难以理解和很好地使用。好消息是,eBay 提供了刚才提到的所有资源,可以帮助作为 eBay 应用程序开发人员的您更加轻松地工作。正如我前面所提到的,公司在让开发人员使用它的 Web 服务 API 编写应用程序方面有既得利益,因而它将使该流程对于您来说尽可能地容易。请利用这一情况并全身心地投入。
更多精彩
赞助商链接