Chapter 4. Project 1: Competitive Analysis
If you sell products, you’re almost certainly interested in watching the market for similar products. Consider one example of this: someone who monitors the book market, keeping track of the performance of a set of titles over time. This chapter shows how to use web services to pull together reports on product information from three different sources—Amazon, Google, and eBay. You’ll build a simple web application that will let an analyst enter product information and have a report emailed that contains the relevant information.
Application Features
User enters and saves search information for a book (the title and an ISBN).
User can create, view, delete, and run saved searches.
The application tracks the following data:
Amazon.com prices, availability, release date, and sales rank based on the supplied ISBN
Google rankings and top five search results for the book title
eBay search results for the book title, including number of matching listings, highest price, and number of bids
A few JSP pages can present the information and accept input
from the user. These JSP pages rely on a simple Java class, Search
. The
Search
class deals with connections to web services via a set of
supporting classes: AmazonConnection
,
EbayConnection
, and GoogleConnection
. This relationship is shown
in Figure 4-1.
First, the following list details the JSP pages and presentation objects used in the application.
- list.jsp
A list of the previously saved searches
- new.jsp
Create a new search
- view.jsp
View the results of a search
- delete.jsp
Delete a saved search
- update.jsp
Request an existing search to be updated
- default.css
Rudimentary stylesheet
Listing Searches
The first JSP, list.jsp , is shown in Figure 4-2. This is the main page for the web application. Clicking View takes users to a report page showing the data retrieved from the various web services. Delete removes the selected search from the web application, and Update causes the web application to refresh the data from the various web services.
The code for list.jsp is shown in Example 4-1. It can conceptually be broken into three main sections. First, there are a few initialization lines, the normal JSP and HTML headers. Next, a JSP scriptlet retrieves data from our supporting Java classes and performs some housekeeping to more cleanly present the data in the JSP. Finally, the actual HTML formatting and JSP display variables appear.
<%@ page contentType="text/html; charset=iso-8859-1" language="java" import="com.cascadetg.ch04.*" errorPage="" %> <% boolean isUpdating = false; java.util.Hashtable mySearches = com.cascadetg.ch04.Search.getAllSearches( ); if(mySearches != null) { java.util.Enumeration mySearchEnum = mySearches.elements( ); while(mySearchEnum.hasMoreElements( )) { Search current = (com.cascadetg.ch04.Search)mySearchEnum.nextElement( ); if(current.isUpdating( )) isUpdating = true; } } %> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Chapter 4: Competitive Analysis, List</title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> <% if (isUpdating) { %> <meta http-equiv="refresh" content="15;url=list.jsp"> <% } %> <link href="default.css" rel="stylesheet" type="text/css" /> </head> <body> <% if (isUpdating) { %> <p align="right">Page will automatic refresh every 15 seconds while an update is in progress.</p> <% } %> <% if (mySearches == null) { %> <p>No searches defined.</p> <% } else { %> <table width="100%" border="0" cellspacing="3" cellpadding="3"> <tr> <td><strong>Search</strong></td> <td width="200" nowrap="nowrap"> </td> </tr> <% java.util.Enumeration mySearchListing = mySearches.elements( ); while(mySearchListing.hasMoreElements( )) { Search mySearch = (Search)mySearchListing.nextElement( ); String productID = mySearch.getProductID( ); %> <tr> <td class="list"> <%= mySearch.getProductTitle( ) %> (<%= productID %>)</td> <td width="200" align="center" nowrap="nowrap" class="list"> <a href="view.jsp?productID=<%=productID %>">View</a> - <a href="delete.jsp?productID=<%=productID %>">Delete</a> - <a href="update.jsp?productID=<%=productID %>">Update</a></td> </tr> <% } %> </table> <% } %> <p><a href="new.jsp">New Search</a></p> </body> </html>
Adding Searches
A user can click on the New Search link (as shown in Figure 4-2) to be brought to a form to add a new search. An example of this form is shown in Figure 4-3.
The book title field is self-explanatory. The ISBN field is provided to allow entry of an International Standard Book Number (ISBN), a worldwide standard for providing a unique identifying number for any published book.
The ISBN number can typically be found on the back cover of a book. You can also find ISBN numbers for books by browsing the Amazon web site or consulting with the publisher. If you just want to try the application without finding a new ISBN, you can click on the provided link.
Note
Another, different standard from ISBN is the Universal Product Code (UPC). Often, both the creator and distributors of a product introduce additional identifying codes for goods and services. Which codes to use is usually a business-driven question, but you’ll want to be aware of what numbering systems are in place.
In this example, we’re using the ISBN as the unique identifier for the books we’re searching for, but in the “real world,” you may want to use your own internal tracking system and then map that to the other systems. For example, if you carry both books (ISBN) and pickles (UPC), you’re using two different numbering systems.
The code for new.jsp file is fairly straightforward, as shown in Example 4-2. If the creation of a new search is successful, users are returned to list.jsp.
<%@ page contentType="text/html; charset=iso-8859-1" language="java" errorPage="" %> <% boolean success = false; boolean submitted = (request.getParameter("Submit") != null); if(submitted) { String productID = request.getParameter("productID"); String productTitle = request.getParameter("productTitle"); success = com.cascadetg.ch04.Search.addSearch(productID, productTitle); if(success) { response.sendRedirect("list.jsp"); return; } } %> <head> <title>Chapter 4: Competitive Analysis</title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> <link href="default.css" rel="stylesheet" type="text/css" /> </head> <body> <% if(submitted) { %> <p>Error submitting. Try again.</p> <% } %> <form name="new_item" id="new_item" method="post" action="new.jsp"> <table width="60%" border="0" cellspacing="0" cellpadding="0"> <tr> <td width="50%">Book ISBN :</td> <td><input name="productID" type="text" /></td> </tr> <tr> <td width="50%">Book Title :</td> <td><input name="productTitle" type="text" /></td> </tr> <tr> <td width="50%"> </td> <td><input type="submit" name="Submit" value="Submit" /></td> </tr> </table> <p><a href="new.jsp?productID=0596004001&productTitle= Mac%20OS%20X%20for%20Java%20Geeks&Submit=Submit"> Add Mac OS X for Java Geeks</a> </p> </form> </body> </html>
Viewing a Search
Figure 4-4 shows an example of the results of a search. The user can click the “Return to list” link in the upper-right corner to return to the search list.
As you can see from Example
4-3, view.jsp retrieves the results from the Search
object. The results are then
formatted into a HTML table. The heavy lifting is performed by the
Search.update( )
method, as
described later in this chapter.
<%@ page contentType="text/html; charset=iso-8859-1" language="java" errorPage="" %> <% String productID = request.getParameter("productID"); com.cascadetg.ch04.Search mySearch = com.cascadetg.ch04.Search.getSearch(productID); Object[] attributeKeys = null; if(mySearch != null) { java.util.Map myAttributes = mySearch.getAttributes( ); if(myAttributes != null) { attributeKeys = myAttributes.keySet( ).toArray( ); } }; %> <head> <title>Chapter 4: Competitive Analysis</title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> <link href="default.css" rel="stylesheet" type="text/css" /> </head> <body> <p align="right"><a href="list.jsp">Return to list</a></p> <% if(mySearch == null) { %> <p>Unable to find this product ID (<%=productID%>)</p> <% } else { %> <h2>Book Title: <%= mySearch.getProductTitle( ) %> (ISBN: <%= mySearch.getProductID( ) %>)</h2> <p>Is Updating: <%= mySearch.isUpdating( ) %></p> <table width="100%" border="0" cellspacing="3" cellpadding="3"> <tr> <td nowrap="nowrap">Attribute</td> <td>Value</td> </tr> <% if(attributeKeys != null) for(int i = 0; i < attributeKeys.length; i++) { String key = (String)attributeKeys[i]; %> <tr> <td nowrap="nowrap" class="list"><%= key %></td> <td class="list"><%= mySearch.getAttribute(key) %></td> </tr> <% } %> </table> <% } %> <p><a href="list.jsp">Return to list</a></p> <p><a href="new.jsp">New search</a></p> </body> </html>
Removing a Search
If we allow people to add searches, we also should allow them to remove them. When a user clicks delete from the main JSP (as shown in Figure 4-2), a simple form (as shown in Figure 4-5) prompts the user to confirm the deletion before processing the request.
The code for this delete confirmation dialog is shown in Example 4-4.
<%@ page contentType="text/html; charset=iso-8859-1" language="java" errorPage="" %> <% boolean delete = false; String productID = request.getParameter("productID"); if(request.getParameter("Delete") != null) delete = true; boolean deleted = false; if(delete) { deleted = com.cascadetg.ch04.Search.removeSearch(productID); }; %> <head> <title>Chapter 4: Competitive Analysis</title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> <link href="default.css" rel="stylesheet" type="text/css" /> </head> <body><% if (delete) { if(deleted) { %> <p>The item has been deleted.</p> <% } else { %> <p>Item not found.</p> <% } } else{ %> <p>Are you sure you want to delete the item "<%= productID %>"?</p> <form name="deleteForm" id="deleteForm" method="post" action="delete.jsp"> <input type="hidden" name="productID" value="<%= productID %>"/> <input name="Delete" type="submit" id="Delete" value="Delete" /> </form> <% } %> <p><a href="list.jsp">Return to list.</a></p> </body> </html>
Updating a Search
The Search
object caches the
data returned from the various web services in memory, but a user may
wish to force the system to immediately refresh the data. Clicking on
the update link on the main page (Figure 4-2) allows a user to do
just that, after a confirmation web page, as shown in Figure 4-6.
The code for the update confirmation page is shown in Example 4-5.
<%@ page contentType="text/html; charset=iso-8859-1" language="java" import="com.cascadetg.ch04.*" errorPage="" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <% boolean updateProduct = false; String productID = request.getParameter("productID"); if(productID != null) { updateProduct = true; Search.getSearch(productID).update( ); } %> <title>Chapter 4: Competitive Analysis</title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> <link href="default.css" rel="stylesheet" type="text/css" /> </head> <body> <% if (updateProduct) { %> <p>Product updated.</p> <% } else { %> <p>Unknown product.</p> <% } %> <p><a href="list.jsp">Return to list.</a> </p> </body> </html>
Gathering Web Service Data
The various user interface elements, as shown so far, all
rely on a single Search
class for the actual web service data retrieval; the JSP
pages don’t contain any code that actually communicates with web
services. Instead, a single Search
object relies on supporting classes, as shown in Figure 4-7, to retrieve the
data.
The four static methods, addSearch(
)
, getAllSearches( )
,
getSearch( )
, and removeSearch( )
, are the main points of
interest to the JSP pages. All data about the web services results is
contained in a set of simple name/value pairs, contained in the java.util.Hashtable
variable attributes
. When a search is run, the Search
object is passed to the three
supporting web service connection classes, and the appropriate data is
set in attributes
.
The code shown in Example
4-6 shows the code for the Search
class. Pay particular interest to the
update( )
method.
package com.cascadetg.ch04; import java.util.Hashtable; public class Search { // Static search Hashtable used for persistence. // This means that the data stored by this application is lost // every time the server is restarted. Adding support for // persistence is left as an exercise for the reader - the most // likely, of course, being saving the data to a database in some // fashion. // If you are interested in adding persistence, you'll want to // intercept the addSearch( ), removeSearch( ), and update( ) methods // in this class. private static Hashtable searches = new Hashtable( ); public static boolean addSearch( String productID, String productTitle) { if (productID == null) return false; if (productID.length( ) < 1) return false; if (productTitle == null) return false; if (productID.length( ) < 1) return false; if (searches.get(productID) != null) searches.remove(productID); Search mySearch = new Search( ); mySearch.setProductID(productID); mySearch.setProductTitle(productTitle); searches.put(productID, mySearch); mySearch.update( ); return true; } public static boolean removeSearch(String productID) { if (searches == null) { return true; } if (searches.get(productID) == null) return false; searches.remove(productID); return true; } public static Hashtable getAllSearches( ) { return searches; } public static Search getSearch(String inProductID) { return (Search)searches.get(inProductID); } // Simple object data. Note the use of a Hashtable to store // a variable set of attributes private String productID; private String productTitle; private Hashtable attributes = new Hashtable( ); public void setAttribute(String name, String value) { attributes.put(name, value); } public String getAttribute(String name) { return (String)attributes.get(name); } public java.util.Map getAttributes( ) { java.util.TreeMap map = new java.util.TreeMap(java.text.Collator.getInstance( )); map.putAll(attributes); return map; } /** Returns the product ID (specifically, a book ISBN). */ public String getProductID( ) { return productID; } /** Set the product ID (specificially, a book ISBN). */ public void setProductID(String productID) { this.productID = productID; } public String getProductTitle( ) { return productTitle; } public void setProductTitle(String productTitle) { this.productTitle = productTitle; } // Updating data logic // Note that this updating logic isn't as useful as it should be, // as the update( ) ought to kick off a new thread. private boolean is_updating = false; public boolean isUpdating( ) { return is_updating; } public void update( ) { // Don't allow the user to set up dozens of hits on the site at // once. Ideally, you should be more conservative about kicking // off requests, perhaps using an aggregation approach (as // described in Chapter 8, News Aggregator if (is_updating) return; is_updating = true; // Create a new Search and set the values to the existing // search Search mySearch = new Search( ); mySearch.setProductID(this.productID); mySearch.setProductTitle(this.productTitle); // Kick off the requests to the various web services new AmazonConnection( ).addAttributes(mySearch); new EbayConnection( ).addAttributes(mySearch); new GoogleConnection( ).addAttributes(mySearch); // Replace the old attributes with the newly discovered values this.attributes = mySearch.attributes; is_updating = false; } }
It’s easy to imagine enhancements: the underlying system might
implement a Connection
interface,
with the potential to add new web services dynamically.
One other thing you may have noticed: the getAttributes()
method returns a java.util.Map
, instead of the underlying
Hashtable
. As shown in Example 4-7, the code creates a
TreeMap
that keeps the attributes
sorted by the system’s default locale interpretation of an
“alphabetical” sorting routine.
java.util.TreeMap map = new java.util.TreeMap(java.text.Collator.getInstance( )); map.putAll(attributes);
Now, on to the real meat of the application—the web services connectivity.
Connecting to Amazon
The Amazon web services package is implemented according to the commonly regarded interpretation of web services—a series of SOAP methods, complete with a WSDL file describing their offerings. We will use Axis, as described in Chapter 3, to work with Amazon’s web services.
Before working with Amazon’s web services, you must register with Amazon’s developer program at http://www.amazon.com/webservices/, as shown in Chapter 4.
The instructions are straightforward: you must download the developer kit (a single file, kit.zip) and get a web services token. This token, a simple string, is used by Amazon to uniquely identify you when you are accessing web services. You should treat this token as you would any password.
As shown in Figure 4-9, you’ll find a number of examples for a variety of programming languages as well as the documentation.
Open the file kit/API Guide/index.html in your web browser to see the documentation, as shown in Figure 4-10.
In our case, we’ll use Axis to generate access code from the
Amazon-supplied WSDL file. The Amazon WSDL file, as described in the
READMEFIRST.txt file, can be found at http://soap.amazon.com/schemas3/AmazonWebServices.wsdl.
This WSDL file is used by the Axis WSDL2Java
tool to generate a set of Java
bindings. The easiest way to do this is to open a command prompt and
navigate to the Axis library installation directory. Then, execute the
following command, all as one line, as shown in Example 4-8. Note that you need an
Internet connection for this to work.
If successful, there will be no visible output on the command
line, but you will now have a set of Java classes corresponding to the
Amazon SDK in the Axis directory. As shown in Figure 4-11, these classes are
neatly sorted into a com.amazon.soap
package. Although you are
given the source to these classes by this tool, you won’t want to edit
them manually. You will likely want to regenerate these files any time
Amazon adds new functionality.
Copy these files out of this directory and into your Java source tree.
The bulk of the generated classes essentially serve as data
holders for Amazon web service operations. To access the web services,
use the AmazonSearchServiceLocator
class to retrieve a service and an AmazonSearchPort
to handle web service
requests. The AmazonSearchServiceLocator
class hierarchy is shown in Figure 4-12.
After retrieving a service, you use a search port object to
perform requests. The generated class hierarchy is shown in Figure 4-13. Note that the
methods of the AmazonSearchPort
class take instances of other classes generated by the Axis toolkit as
parameters.
The code shown in Example
4-9 shows how to actually make a connection to the Amazon web
services. In this case, an instance of the AsinRequest
class is used. The call to myAmazonSearchPort.asinSearchRequest(myAsinRequest)
actually performs the request. It transforms the underlying data to
SOAP, sends the data over the network, retrieves the resulting data,
and transforms that data back into Java object(s), in this case a
ProductInfo
object.
package com.cascadetg.ch04; import java.util.Date; import com.amazon.soap.*; public class AmazonConnection { public void addAttributes(Search inSearch) { inSearch.setAttribute( "Amazon Last Check", new Date( ).toLocaleString( )); // Mac OS X for Java Geeks ISBN //String isbn = "0596004001"; try { AmazonSearchService myAmazonSearchService = new AmazonSearchServiceLocator( ); AmazonSearchPort myAmazonSearchPort = myAmazonSearchService.getAmazonSearchPort( ); AsinRequest myAsinRequest = new AsinRequest( ); // Use this to set your Amazon Associates ID // For more info on Amazon Associates, see... // http://www.amazon.com/associates myAsinRequest.setTag(DeveloperTokens.amazon_associates); myAsinRequest.setDevtag(DeveloperTokens.amazon_token); myAsinRequest.setAsin(inSearch.getProductID( )); myAsinRequest.setLocale("us"); myAsinRequest.setType("heavy"); ProductInfo myProductInfo = myAmazonSearchPort.asinSearchRequest(myAsinRequest); Details[] myDetailsArray = myProductInfo.getDetails( ); Details myDetail = null; if (myDetailsArray != null) { myDetail = myDetailsArray[0]; inSearch.setAttribute( "Amazon Product Name", myDetail.getProductName( )); inSearch.setAttribute( "Amazon Release Date", myDetail.getReleaseDate( )); inSearch.setAttribute( "Amazon Actual Price", myDetail.getOurPrice( )); inSearch.setAttribute( "Amazon List Price", myDetail.getListPrice( )); inSearch.setAttribute( "Amazon Used Price", myDetail.getUsedPrice( )); inSearch.setAttribute( "Amazon Sales Rank", myDetail.getSalesRank( )); inSearch.setAttribute( "Amazon Availability", myDetail.getAvailability( )); //myDetail.getImageUrlSmall( ); //myDetail.getImageUrlMedium( ); //myDetail.getImageUrlLarge( ); } } catch (Exception e) { e.printStackTrace( ); } } }
The code shown in Example
4-9 is as close to a standard RPC system as a web service is
likely to be. None of the actual connectivity details are in the code:
all of the URLs, ports, and other connectivity information are in the
WSDL and then embedded in the code generated by the WSDL2Java
tool.
Connecting to eBay
eBay’s web service implementation provides access to their systems via XML over HTTPS. A simple HTTPS connection provides in-transit security, and XML data is sent and received to perform operations. While establishing an HTTPS connection to the eBay servers is a relatively trivial matter, working with the XML documents is more time-consuming.
As of mid-2004, there are several tiers of access offered by eBay, based more or less on the quantity of web service requests you require. The free, individual developer version allows for only 50 calls a day—not very many, but likely enough to build and do some basic testing of your application. The next step up, called Basic, offers 30,000 calls per month for $500 per year.
By default, you have access to the eBay sandbox server, a test environment set up for developers to test their application without incurring service fees. To access the production server, you must be certified by eBay (as of this writing, eBay charges $100 for certification). If you’re looking at building a production application that accesses eBay services, you need to include these access and certification fees in your budget. Don’t forget that it can take several days to get your application certified.
The eBay sandbox, http://sandbox.ebay.com/, shown in Figure 4-14, is a good development resource. It’s a recreation of the eBay web site (http://www.ebay.com/) and is intended for developers to test their application. There are several portions of the web site that aren’t implemented or work slightly differently; for example, you can’t actually enter a credit card and authenticate a user to sell items on the test server. Instead, you use eBay web services to do this programmatically using the web services API.
The eBay developer registration system is a bit more complex
than other offerings in order to support more complex deployment and
usage models. First, you need to register for eBay itself and then go
through the six-step registration process (http://developer.ebay.com/ → Membership → Join). When you’re done registering, you can
generate a set of three security tokens, a Developer ID (devID
), Application ID (appID
), and Certification ID (certID
). Make sure that your pop-up blocking
software is disabled when you generate your security tokens.
Warning
Don’t lose your eBay security tokens! You can generate them for the first time online, but if you lose them, you’ll have to go through eBay support. It can take several days to verify your information and receive new tokens.
eBay API wrapper
In order to make it easier to connect to eBay in this chapter (and others), the sample applications use a standard eBay connectivity class, as shown in Figure 4-15, as a reusable component for connecting to eBay’s XML-via-HTTPS web services.
The code for the eBay class is shown in Example 4-10. All the details
of opening a connection to eBay, sending the XML, and then
retrieving the results are wrapped by this class. The code is
designed to make a call as simple as possible: create a new EbayAPISimpleCall
object, set the eBay
“verb” you wish to call, and add the arguments you need with
EbayAPISimpleCall.setArgument()
.
package com.cascadetg.ch04; import java.net.*; import java.io.*; import java.util.Hashtable; /** * Based on the eBay sample code, APISimpleCallJava.java */ public class EbayAPISimpleCall { // The eBay API function private String apiVerb = "GeteBayOfficialTime"; /** API Verb used to get the official eBay time */ public static final String GetOfficialTime = "GeteBayOfficialTime"; /** API Verb used to search on eBay */ public static final String GetSearchResults = "GetSearchResults"; // The version threshold of compatibility for the eBay API call private String compatibilityLevel = "331"; // The error level for the API call private String errorLevel = "1"; // Set siteId to correspond to the US Site private String siteId = "0"; // "0" is the only supported detail level for GetebayOfficialTime private String detailLevel = "0"; public EbayAPISimpleCall( ) { } boolean isProduction = false; public org.jdom.Document executeCall( ) { // Supply a URL to the desired API server, for example: // Sandbox: https://api.sandbox.ebay.com/ws/api.dll // Production: https://api.ebay.com/ws/api.dll String targetURL; if (!isProduction) { targetURL = "https://api.sandbox.ebay.com/ws/api.dll"; } else { targetURL = "https://api.ebay.com/ws/api.dll"; } // This string will contain the assembled XML for the API call String eBayXML; try { // Create a new URL object using the specified targetURL URL website = new URL(targetURL); // Create a connection to the URL Object foo = website.openConnection( ); // Note that this is actually an SSL connection, even // though cast to an HttpURLConnection. Different JDK // versions use a different SSL connection class, but they // all use HttpURLConnection as the base class (which has // the methods we need). HttpURLConnection connection = (HttpURLConnection)website.openConnection( ); // Specify that the connection will be used for input and // output connection.setDoInput(true); connection.setDoOutput(true); // Specify the method for the URL request connection.setRequestMethod("POST"); // Add the eBay specific headers needed for an eBay API // call // see: "Making the HTTP request" in the eBay API // documentation addeBayAPIHeaders(connection); // Build the XML call String for eBayGetOfficialTime eBayXML = buildBaseXML( DeveloperTokens.eBay_userid, DeveloperTokens.ebay_userPassword) + buildVerbCall( ); // Create the Output and Print Streams OutputStream output = connection.getOutputStream( ); PrintStream pout = new PrintStream(output); // 'Upload' the eBayXML String pout.print(eBayXML); pout.close( ); // Create the Input Streams InputStream input = connection.getInputStream( ); BufferedInputStream bufIn = new BufferedInputStream(input); org.jdom.Document result = new org.jdom.input.SAXBuilder( ).build(bufIn); connection.disconnect( ); return result; } catch (MalformedURLException ex) { ex.printStackTrace( ); } catch (IOException ioException) { ioException.printStackTrace( ); } catch (org.jdom.JDOMException JDOMprob) { JDOMprob.printStackTrace( ); } return null; } // A test routine to verify that everything is working. public static void main(String[] args) throws IOException { EbayAPISimpleCall myCall = new EbayAPISimpleCall( ); myCall.setApiVerb(GetOfficialTime); org.jdom.Document myDocument = myCall.executeCall( ); new org.jdom.output.XMLOutputter( ).output( myDocument, System.out); myCall.setApiVerb(GetSearchResults); myCall.setArgument("Query", "test"); myDocument = myCall.executeCall( ); new org.jdom.output.XMLOutputter( ).output( myDocument, System.out); String result = myDocument.getRootElement( ).getChild("eBayTime").getText( ); System.out.println(result); result = myDocument .getRootElement( ) .getChild("Search") .getChild("GrandTotal") .getText( ); System.out.println(result); } private void addeBayAPIHeaders(HttpURLConnection connection) { // Generate and add the Session Certificate Header connection.addRequestProperty( "X-EBAY-API-SESSION-CERTIFICATE", DeveloperTokens.ebay_devId + ";" + DeveloperTokens.appId + ";" + DeveloperTokens.certId); // Add the Compatibility Level Header connection.addRequestProperty( "X-EBAY-API-COMPATIBILITY-LEVEL", compatibilityLevel); // Add the Developer Name, Application Name, and Certification // Name headers connection.addRequestProperty( "X-EBAY-API-DEV-NAME", DeveloperTokens.ebay_devId); connection.addRequestProperty( "X-EBAY-API-APP-NAME", DeveloperTokens.appId); connection.addRequestProperty( "X-EBAY-API-CERT-NAME", DeveloperTokens.certId); // Add the API verb Header connection.addRequestProperty("X-EBAY-API-CALL-NAME", apiVerb); // Add the Site Id Header connection.addRequestProperty("X-EBAY-API-SITEID", siteId); // Add the Detail Level Header connection.addRequestProperty( "X-EBAY-API-DETAIL-LEVEL", detailLevel); // Add the Content-Type Header connection.addRequestProperty("Content-Type", "text/xml"); // NOTE: eBay recommends setting the Content-Length header // see: "Making the HTTP request" in the eBay API documentation } private String buildBaseXML(String userId, String userPassword) { return "<?xml version='1.0' encoding='utf-8'?>\r\n" + "<request>\r\n" + "<RequestUserId>" + userId + "</RequestUserId>\r\n" + "<RequestPassword>" + userPassword + "</RequestPassword>\r\n"; } Hashtable arguments = new Hashtable( ); public void clearArguments( ) { arguments = new Hashtable( ); } public Hashtable getArguments( ) { return arguments; } public void setArgument(String argument, String value) { arguments.put(argument, value); } public String getArgument(String argument) { return (String)arguments.get(argument); } public void setArguments(Hashtable arguments) { this.arguments = arguments; } private String buildVerbCall( ) { StringBuffer verbCall = new StringBuffer( ); verbCall.append("<ErrorLevel>"); verbCall.append(errorLevel); verbCall.append("</ErrorLevel>\r\n"); verbCall.append("<Verb>"); verbCall.append(apiVerb); verbCall.append("</Verb>\r\n"); verbCall.append("<DetailLevel>"); verbCall.append(detailLevel); verbCall.append("</DetailLevel>\r\n"); verbCall.append("<SiteId>"); verbCall.append(siteId); verbCall.append("</SiteId>\r\n"); if (arguments.size( ) > 0) { java.util.Enumeration keys = arguments.keys( ); while (keys.hasMoreElements( )) { String current = (String)keys.nextElement( ); verbCall.append("<"); verbCall.append(current); verbCall.append(">"); verbCall.append(arguments.get(current)); verbCall.append("</"); verbCall.append(current); verbCall.append(">"); } } verbCall.append("</request>"); return verbCall.toString( ); } /** * @return Returns the detailLevel. */ public String getDetailLevel( ) { return detailLevel; } /** * @param detailLevel * The detailLevel to set. */ public void setDetailLevel(String detailLevel) { this.detailLevel = detailLevel; } /** * @return Returns the errorLevel. */ public String getErrorLevel( ) { return errorLevel; } /** * @param errorLevel * The errorLevel to set. */ public void setErrorLevel(String errorLevel) { this.errorLevel = errorLevel; } /** * @return Returns the siteId. */ public String getSiteId( ) { return siteId; } /** * @param siteId * The siteId to set. */ public void setSiteId(String siteId) { this.siteId = siteId; } /** * @return Returns the compatibilityLevel. */ public String getCompatibilityLevel( ) { return compatibilityLevel; } /** * @param compatibilityLevel * The compatibilityLevel to set. This controls the * versioning of the XML interface for the API. This is * how eBay allows multiple versions of their API to be * supported simultaneously. */ public void setCompatibilityLevel(String compatibilityLevel) { this.compatibilityLevel = compatibilityLevel; } /** * @return Returns the isProduction. */ public boolean isProduction( ) { return isProduction; } /** * @param isProduction * By default (isProduction = false), the server will * contact the eBay test environment, called the * "sandbox." If you wish to use the production * environment, you'll need to set this to true. */ public void setProduction(boolean isProduction) { this.isProduction = isProduction; } public String getApiVerb( ) { return apiVerb; } /** * Two standard verbs are provided with this class, * GeteBayOfficialTime and GetSearchResults. Others can be added * from the eBay documentation. */ public void setApiVerb(String apiVerb) { this.apiVerb = apiVerb; } }
For eBay, a verb is
the term that describes the name of a web service method. Each verb
describes a particular action, with a set of one or more possible
parameters. These verbs generally have self-describing names, such
as GeteBayOfficialTime
or
GetSearchResults
. eBay’s
documentation lists many verbs; using our EbayAPISimpleCall
. setApiVerb()
method, it’s easy to call any
of them. Two static String
objects are defined in this class; they correspond to the official
eBay verbs. An enterprising developer might imagine developing a
custom class hierarchy corresponding to the eBay framework. However,
keep in mind that this class hierarchy quickly looks like the
bindings generated by a SOAP framework!
When you’re ready, call EbayAPISimpleCall.executeCall( )
, and
you’ll receive a result via a JDOM object (which holds the XML
returned by eBay). If the result is an eBay error message, that is
the contents of the XML; otherwise, it should be the data you
need.
The code to make the call into eBay is only support
infrastructure; it doesn’t actually help you write your application.
It’s analogous to the code generated by the WSDL2Java
code in the previous example
(talking to Amazon), but in this case, you have to maintain it
yourself.
eBay connectivity
Now that you have a reusable wrapper for accessing eBay web services, it’s time to look at the code to actually get useful data back from the eBay service.
As mentioned earlier, you must create and validate a user on
the sandbox in order to perform certain activities, such as
searches. The main( )
method of
the code shown in Example
4-11 can be used to authenticate a user as a command-line
utility if needed. The addAttributes()
method is called by the Search
object to retrieve data from eBay—a
straightforward operation, given the wrapper shown in Example 4-10.
package com.cascadetg.ch04; import java.util.Date; import org.jdom.output.XMLOutputter; public class EbayConnection { public org.jdom.Document validateSandboxUser( String username, String password) { EbayAPISimpleCall myCall = new EbayAPISimpleCall( ); // Validate this user on the sandbox server only myCall.setProduction(false); myCall.setApiVerb("ValidateTestUserRegistration"); myCall.setArgument("RequestUserId", username); myCall.setArgument("RequestPassword", password); return myCall.executeCall( ); } /** * Call this class, passing in the username and password you would * like to register on the eBay test server. * * @param args */ static public void main(String[] args) { if (args == null) { System.out.println("Need two arguments (name & password)."); return; } if (args.length < 2) { System.out.println("Need two arguments (name & password)."); return; } try { new XMLOutputter( ).output( new EbayConnection( ).validateSandboxUser( args[0], args[1]), System.out); } catch (Exception e) { System.out.println("Unable to register user."); e.printStackTrace( ); } } public void addAttributes(Search inSearch) { inSearch.setAttribute( "eBay Last Check", new Date( ).toLocaleString( )); EbayAPISimpleCall myCall = new EbayAPISimpleCall( ); myCall.setApiVerb(EbayAPISimpleCall.GetSearchResults); myCall.setArgument("Query", inSearch.getProductTitle( )); myCall.setArgument("Order", "MetaHighestPriceSort"); org.jdom.Document myResults = myCall.executeCall( ); org.jdom.Element root = myResults.getRootElement( ); long count = Long.parseLong( root .getChild("Search") .getChild("GrandTotal") .getText( )); inSearch.setAttribute( "eBay Total Matching Listings", Long.toString(count)); if (count > 0) { org.jdom.Element item = root.getChild("Search").getChild("Items").getChild( "Item"); inSearch.setAttribute( "eBay Highest Bid Item Price", item.getChildText("LocalizedCurrentPrice")); inSearch.setAttribute( "eBay Highest Bid Item Bids", item.getChildText("BidCount")); inSearch.setAttribute( "eBay Highest Bid Item Link", item.getChildText("Link")); } } }
Lets take a closer look at the connectivity code, as shown in
Example 4-12. It starts
by creating a object to represent the call to eBay, EbayAPISimpleCall
. It then sets the eBay
verb to use. In the sample, a search is being performed, but you can
find documentation on other supported verbs at http://developer.ebay.com/DevZone/docs/API_Doc/index.asp.
Next, the arguments are set. For a search query, the search
terms must be specified—in this case, the title of the book. The
application also needs the highest value auction result, so a second
argument, Order
, is added with
the string MetaHighestPriceSort
(as specified by the eBay documentation for the search verb). The
call is then issued, and an XML document containing the results of
the query is returned. From there, it’s just a question of using the
JDOM interfaces to walk through the returned XML.
To more fully support this, you may wish to perform more robust error handling (indeed, you’ll be required to do this to pass eBay’s certification), but it shows how even with a nonstandard API, a bit of work can decouple the details of establishing the network connection from the rest of your application logic.
Connecting to Google
After the previous examples, it may surprise you to see how easy it is to connect to Google’s web service offerings. Visiting http://www.google.com/apis/, shown in Figure 4-16, it’s pretty straightforward to start building applications using the Google web service interface. Make sure you download the developer kit as shown on the front page; you will need the provided libraries.
Google does offer APIs via SOAP and WSDL, but for Java
developers, there’s a pre-built library that does all the work for
you. It’s as if they took the time to run Axis WSDL2Java
for you, and then just provided
you with a library to use (in fact, that’s very close to what they
actually did). It’s entirely possible to use Axis to generate the Java
code from their WSDL file (posted at http://api.google.com/GoogleSearch.wsdl) just as you
did for Amazon. Instead, you should rely on the Java library they
provided, called googleapi.jar, as shown in Figure 4-17. You must add this
library to your class path. For more information on the Google API,
open the APIs_Reference.html file.
The code as shown in Example
4-13 could hardly be simpler. More lines are spent looping
through the results and adding them to the Search
object’s attributes than it takes to
get data back from the Google server itself. The code converts the
GoogleSearchResult
objects to
String
s and add them to the results
given, but it’s easy to imagine using the various methods on the
GoogleSearchResult
object to get
more detail and provide even better formatting.
package com.cascadetg.ch04; import java.util.Date; import com.google.soap.search.*; public class GoogleConnection { public void addAttributes(Search inSearch) { inSearch.setAttribute( "Google Last Check", new Date( ).toLocaleString( )); GoogleSearch search = new GoogleSearch( ); // Set mandatory attributes search.setKey(DeveloperTokens.googleKey); search.setQueryString(inSearch.getProductTitle( )); // Set optional attributes search.setSafeSearch(true); // Invoke the actual search GoogleSearchResult result = null; try { result = search.doSearch( ); } catch (GoogleSearchFault e) { e.printStackTrace( ); } // process the result if (result != null) { inSearch.setAttribute( "Google Number of Hits", Integer.toString( result.getEstimatedTotalResultsCount( ))); GoogleSearchResultElement[] mySearchElements = result.getResultElements( ); for (int i = 0; i < mySearchElements.length; i++) { inSearch.setAttribute( "Google Result " + i, mySearchElements[i].toString( )); if (i > 4) { i = mySearchElements.length; } } } } }
Developer Tokens
You may have noticed references to a DeveloperTokens
class (e.g. DeveloperTokens.amazon_associates
, or
DeveloperTokens.amazon_token
). This
class is a single static class with a set of String
s containing the various developer
tokens used throughout this chapter. The class shown in Example 4-14 contains only
placeholder strings of the approximate correct length.
package com.cascadetg.ch04; public class DeveloperTokens { // Amazon userID token public static String amazon_token = "12345678901234"; // Amazon assoicates ID public static String amazon_associates = "123456789-20"; // eBay userID (the "visible" user identification, i.e. the buyer & // seller user. public static String eBay_userid = "1234567890"; public static String ebay_userPassword = "1234567890"; // devId, appId, certId as specified by the eBay Developer's // Program public static String ebay_devId = "123456789012345678901234567890"; public static String appId = "123456789012345678901234567890"; public static String certId = "123456789012345678901234567890"; // Google web services ID key public static String googleKey = "12345678901234567890123456789012"; }
As you’ve seen, actually making the connection to a provider isn’t that hard (especially after creating bindings or utility classes to make it easier to access). Different vendors have different registration systems, pricing models, and different expectations in terms of security, but at least all offer free access to their system for developers to start building and testing applications, and all have published documentation and interfaces.
Get Real World Web Services now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.