Similar to Hibernate, Objectify-Appengine makes it easier to map and leverage POJOs against the Google App Engine datastore, also known as Bigtable. In the first half of this article, I introduced Objectify and some of its key features, including JPA annotations (of which it uses only a subset) and custom annotations that integrate with the GAE datastore. I also explained Objectify's approach to relationships and demonstrated its query interface, which supports the GAE notions of filtering and sorting.
Part 2 shifts the focus from domain modeling to finalizing a web application in preparation for deploying it on GAE. First, I want to spend just a few minutes more on the domain model, however, so that I can introduce Objectify's features for indexing and caching. Both features are pretty important when deploying applications to GAE, especially if you plan to move massive amounts of data.
Indexing and caching with Objectify-Appengine
By default, all properties defined in a domain object (and consequently, Google's low-level
Entity API) are indexed. As is true in the relational world, indexing makes searching easier for the underlying datastore, and therefore faster for end users. But indexing isn't cost-free: when new objects are created — such as a new row in relational terms or a new
Entity in Bigtable — you have to update your indexes to include the new data.
Once you start to leverage GAE and store and retrieve real data from it, indexing
becomes a real-world (as in real dollar) concern. When you sign up for a GAE account,
you automatically get 200 indexes for free; beyond that, there is a price. Likewise,
if you exceed 6.50 CPU hours per day, you'll start to rack up the Google debt. So it makes sense to index selectively. If you don't need a property indexed (that is, you don't plan on searching a domain object by that individual property), then turning off that index probably makes sense. In Listing 1, I show you how to use Objectify's
@Unindexed annotation to turn off a particular index.
I've extended the
Retweet object from Part 1, adding some indexing functionality. You can see the changes in the sample code packet. In Listing 1, I've used
@Unindexed to unindex the
Retweet's user's real-name property, which is dubbed
userName as opposed to
screenName. I also unindexed the user's picture URL, a new feature that lets me create a nicer report on the UI.
I don't plan on searching for
Retweets by these properties,
so it makes sense to unindex them. Of course, I can remove the
@Unindexed annotation if something changes. Listing 1 shows how I've enhanced the
Retweet object's properties.
Listing 1. Retweet reloaded
public class Retweet @Id private String id; private String screenName; private Date date; private String tweet; private Long influence; private Key<User> owner; private Long tweetId; @Unindexed private String userName; @Unindexed private String userPicture; //... }
The two bolded properties at the end of the listing —
userName and
userPicture— are new, and both are unindexed. I've renamed the original
userName to
screenName for better integration with Twitter. (In Twitter, your user name is your given name — "Andrew Glover" — and your screen name is your handle — "aglover".)
Storing data in memcache
Like indexing, caching improves the end user experience: it cuts out the roundtrip to
the datastore and back, thus making reads faster. Google App Engine uses memcache for
caching data, and Objectify uses the
@Cached annotation to
plug into it. Just add
@Cached to your domain objects and
—presto!— the data (not the corresponding Java™ object) is stored in memcache and can be retrieved from memory on application reads.
I've added the
@Cached annotation to both of my domain
objects —
User and
Retweet. Listing 2 reveals the updated
User object and also shows off a couple more
@Unindexed properties:
Listing 2. Caching is simple!
@Cached public class User { @Id private String name; @Unindexed private String token; @Unindexed private String tokenSecret; //... }
Note that Objectify's
query calls still hit the datastore;
it's all other datastore interaction calls, like
get, that
leverage memcache. For instance, in the
User object in
Listing 2, the
findByName method leverages Objectify's
get call to load the corresponding domain object. (Recall that the name is the key.) If the
User had already been created then its data would be cached, with subsequent calls to
findByName retrieved from the cache and not the datastore. GAE's quota on memcache calls is higher than on datastore calls, so it makes sense to use memcache whenever possible.
Deploying to GAE
I'm done updating my domain objects, at least in terms of what I can do with Objectify. The next step is to deploy the application to GAE. As I explained last month, my intention is to build a web application that allows users to mine their retweets, being able to see which ones were retweeted by their most influential followers.
At a high level, the web application, which is comprised of
User and
Retweet, will do a number of things: First, the user must authenticate him or herself with Twitter via OAuth. Next, the application will fire off a background process to obtain the user's retweet data. Simultaneously, the application will display a dashboard and make an Ajax call to retrieve the retweet data, which should already be persisted to the GAE datastore (aka Bigtable).
I built the domain aspects of the application last month, so what's left is to integrate it with Twitter's authorization system and then display the corresponding retrieved data.
User authorization with OAuth
As you know if you've read Part 1, Twitter uses OAuth for user authorization. The form of OAuth required depends on whether the application is to be deployed in a desktop, mobile, or web environment. Other than that, there are two things you really need to know about user authorization with OAuth:
- Applications don't need to store a login and password.
- Authorization is delegated to a trusted provider, like Twitter.
Authorization is done with various tokens that are traded. Given that the Retweet app is being deployed to a web environment, the first step is to register it with Twitter. When I register, I provide a few pieces of information, including a callback URL that will be used as a key. I can change the URL at runtime. In return, I receive a consumer key and a consumer secret, which of course I need to keep to myself.
Next, I need the user's permission to sign in to Twitter and grab his or her retweets. The user grants permission by sending some information to Twitter, after which Twitter will invoke my callback URL. In doing so, Twitter will also pass along some tokens (as URL parameters, which can be programmatically obtained).
Authorization workflow
I'm going to use two servlets to handle the authorization workflow. The first servlet will create a
Session object, register a callback URL for Twitter, and shunt the user to Twitter for authentication. The authentication process will also authorize my application to act on the user's behalf, as you'll soon see. The second servlet will be my callback; it will process the response from Twitter and create a corresponding
User object.
Gaelyk is a Groovy framework whose terse syntax and coding shortcuts I'll leverage to quickly build my GAE web application. The first thing I need to do in Gaelyk is to define my application's custom URLs, which I can do using URL routes. Gaelyk's URL routes configuration is essentially a DSL that allows me to (1) build readable URLs and (2) hide the underlying technology. Listing 3 shows the first stage of this mapping:
Listing 3. The beginning of a routes file
get "/login", forward: "/login.groovy" get "/tcallback", forward: "/tcallback.groovy"
In Listing 3, I've defined two custom URLs. Notice how
/login hides the .groovy aspect, making the URL a lot more readable.
The DSL describes a simple sequence: If an
HTTP GET is applied to a URL like
your_domain/login, then forward that request to the
/login.groovy asset, which in Gaelyk's case is a servlet (or groovlet, really).
My login groovlet is also absurdly simple, as you can see in Listing 4:
Listing 4. The login groovlet
import twitter4j.TwitterFactory def twitter = new TwitterFactory().getOAuthAuthorizedInstance("...", "...") def requestToken = twitter.getOAuthRequestToken("") if(!session){ session = request.getSession(true) } session.setAttribute("requestToken_token", requestToken.token) session.setAttribute("requestToken_secret", requestToken.tokenSecret) redirect(requestToken.getAuthorizationURL())
In Listing 4, I use my consumer key and secret to create a
TwitterFactory instance.
TwitterFactory is part of the Twitter4J library. I then obtain a
RequestToken instance, in the process passing in my own
callback URL (, defined in Listing 3), which Twitter will invoke after a user grants access. Lastly, I place two pieces of information into an
HttpSession object. The information will be required when data is transferred back to the web application. A browser is then redirected to an authorization URL on Twitter's website.
The callback handler
My callback handler, defined in Listing 5, simply obtains the required information from Twitter and links that data up with what's found in the
HttpSession object. It then creates a new
User. Alternately, if the datastore already contains a
User object, the groovlet simply updates it with new credential data.
Listing 5. A callback handler
import java.util.Date import twitter4j.TwitterFactory import com.b50.gretweet.User def twitter = new TwitterFactory().getOAuthAuthorizedInstance("...", "...") def accTok = twitter.getOAuthAccessToken( session.getAttribute("requestToken_token"), session.getAttribute("requestToken_secret"), request.getParameter("oauth_verifier")) def scrname = twitter.getScreenName() session.setAttribute("screenname", scrname) new User(scrname, accTok.getToken(), accTok.getTokenSecret()).save() defaultQueue << [ countdownMillis: 1, url: "/retweetpull", taskName: "twitter-pull-${new Date().time}", method: 'POST', params: [id: scrname] ] redirect "/dashboard/${scrname}"
Upon being called, the groovlet obtains a key parameter from Twitter:
oauth_verifier. This parameter, combined with a key and secret, serves as permission to act on the user's behalf going forward. The groovlet also updates the
session object with the user's Twitter handle, after which the
User object is either created or updated.
The code dealing with
defaultQueue is Gaelyk magic for
placing a request onto a GAE queue, which I'll discuss more in the next section. In
the last bit of action in Listing 5, the user's browser is redirected to a new URL, which has the mapping
get "/dashboard/@name", forward: "/dashboard.gtpl?tname=@name". The URL is connected to a web page that receives a parameter named
tname, which is the user's Twitter screen name.
GAE I/O and performance
If you want to build your apps on GAE, there are a few rules you must obey. Google defines what libraries you can and can't use and also places a time limit on servlet response. The upper-bound limit on the time it takes a servlet to handle an incoming request is about 30 seconds. If a servlet takes longer than that time to send out a response, GAE generates an error in the form of an HTTP 500 code.
Google has good reason for imposing the time limit: it needs to be able to scale your application as needed, without your assistance. So if you find that the code you've written requires too much time to process — that is, it interacts with other systems that you don't have control over (or is just plain poorly coded) — then you probably need to rethink your application.
As a GAE developer, you don't have access to the underlying machines your application is running on; in fact, you won't know their configuration or even how much memory is available. But that's probably not where performance issues will surface anyway. I/O is the usual culprit of poor performance, and in GAE, I/O issues will usually pop up when you interact with other systems, like Twitter.
Making API calls to Twitter can be unreliable because (as every Twitter user knows) Twitter occasionally goes down. Response times also can crawl due to volume overload. So building an application that relies on Twitter's data requires a bit of planning. In GAE, it also requires queuing.
The GAE response timer
Google App Engine provides a queuing mechanism that allows tasks to be processed asynchronously in the background. Interestingly, the task itself is a servlet and what you throw onto a queue is a URL, along with optional parameters. You can see all this if you look back to Listing 5. GAE queues allow you to take potentially long-running processes, break them up into succinct tasks (servlets), and throw those tasks onto queues to be executed in the background. Of course, if a task does something interesting, you can save that data into the datastore and retrieve it at some later point — which is what I'm going to do with my user's Twitter retweets.
Once a user has authenticated with Twitter, I'll create a URL to another servlet that obtains retweets via Twitter's API and throw that URL onto a queue. I'm going to use GAE's default queue (named "default"), although I could create a unique queue if I wanted to. Listing 6 shows the code for putting a request onto GAE's default queue, which you first saw in Listing 5:
Listing 6. Putting a request onto a queue
defaultQueue << [ countdownMillis: 1, url: "/retweetpull", taskName: "twitter-pull-${new Date().time}", method: 'POST', params: [id: scrname] ]
I placed a request for the
/retweetpull URL, which is aptly
mapped as
post "/retweetpull", forward:
"/retweetpull.groovy". Note that this is an
HTTP POST
request. I also passed along the parameter
id, which is the
user's screen name. Note that queue task names must be unique. I find it helpful to
append a date-time stamp to create uniqueness. Another option demonstrated in Listing
6 is to define a countdown time that will run until the task is executed.
A task in a GAE queue is just a servlet. Accordingly, a URL is invoked by a queue "reader," and the particular servlet I wish to be executed is shown in Listing 7:
Listing 7. An asynchronously executed servlet
import twitter4j.TwitterFactory import twitter4j.http.AccessToken import com.b50.gretweet.User import com.b50.gretweet.Retweet def user = User.findByName(params['id']) def twitter = getTwitter(user) def retweets = [] def statuses = twitter.getRetweetsOfMe() statuses.each { status -> def tid = status.getId() def dt = status.getCreatedAt() def txt = status.getText() users = twitter.getRetweetedBy(status.getId()) users.each { usr -> retweets << new Retweet(usr.getScreenName(), tid, dt, txt, usr.getFollowersCount(), usr.getProfileImageURL().toString(), usr.getName()) } } user.addRetweets(retweets) def getTwitter(user){ return new TwitterFactory().getOAuthAuthorizedInstance("...", "...", new AccessToken(user.token, user.tokenSecret)) }
The first thing the servlet in Listing 7 does is find a
User
instance, which it actually grabs from memcache. Next, as it's been authorized to do, the servlet acts as the user to establish a Twitter session. With Twitter4J's API, it obtains a list of retweets — for instance,
twitter.getRetweetsOfMe(). It processes each one into a
Retweet object, and places all the
Retweet objects into a
List (using Groovy's handy
<< operator, which is really just an
add method). When the batch is added to the
User instance (via the
addRetweets method), it is saved.
At this point, I've got a few crucial pieces of my web application coded. My next step is to determine how I'll communicate — that is, what format I'll use when issuing a request to grab the application data.
Request format: JSON
Some say that JSON has replaced XML as the new lingua franca of Internet data exchange. JSON is lightweight compared to XML, which makes it easier to read and parse. You can see the difference for yourself by looking at the two listings below. The first listing shows retweet data represented using XML:
<retweet> <screenname>BarackObama</screenname> <influence>5901913</influence> <date>2010-11-11</date> <tweet>"I want our veterans to know: We remember..."</tweet> <tweetid>2758011831459840</tweetid> <picture>...</picture> <username>"Barack Obama"</username> </retweet>
This one shows the same data represented using JSON:
{ "screenname":"BarackObama", "influence":5901913, "date":"2010-11-11" "tweet":"I want our veterans to know: We remember...", "tweetid":2758011831459840, "picture":"...", "username":"Barack Obama" }
Note that the JSON document looks a lot like a map. There are names, which have values. This makes representing something like a domain object quite simple, but don't let JSON's simpleness fool you. JSON can represent lists, which can also be values. JSON's markup is less verbose than that of XML, and the corresponding data is easier to visually comprehend. JSON's terseness can also be an advantage in Ajax systems, which tend to tax browser processing and network bandwidth.
For these reasons, I prefer JSON to XML for
browser-to-server communication. This is especially true for applications that use
Ajax on the front end, as this one will. Just as
java.lang.Object's
toString method
provides a convenient mechanism for codifying an object, I find it helpful to make
domain objects in a web application JSON-able via a
toJSON
method. My first step is to enforce a JSONable contract via the interface in Listing 8:
Listing 8. A toJSON contract
public interface JSONable { String toJSON(); }
Next, I have my domain objects implement the interface. I provide the behavior via a
handy library known as
JSON-lib, which has a simple,
low-level API for creating JSON structures. For example, to represent a
Retweet, my
toJSON method will look like
Listing 9:
Listing 9. Retweet's toJSON
public String toJSON() { JSONObject json = new JSONObject(); json.put("screenname", this.screenName); json.put("influence", this.influence); json.put("date", this.getFormattedDate()); json.put("tweet", this.tweet); json.put("tweetid", this.tweetId); json.put("picture", this.userPicture); json.put("username", this.userName); return json.toString(); }
Note that I've chosen to have explicit control over how my JSON document looks. I'm not
interested in camel-casing, although I've obeyed it by convention in my Java code. So
userName is a property in
Retweet, but I've left it as
username in
my JSON document. In Listing 9, I've used
JSON-lib's
JSONObject and its
put call to define each property I want represented. I've also left out the
owner property.
Representing collections
The pattern of having individual objects represent themselves as JSON is helpful when
building aggregate JSON documents. For example, if I want to represent a collection of
retweets (say the top three retweets as ranked by influence), I can do so. Given a
collection of three
Retweet objects, making a larger JSON
document to contain them is as easy as coding up a servlet that does it all, as in
Listing 10:
Listing 10. Analysis servlet
import com.b50.gretweet.User import com.b50.gretweet.Retweet import net.sf.json.JSONObject response.contentType = "application/json" def user = User.findByName(params['name']) def tweets = user.listAllRetweetsByInfluence() def topthree = tweets.unique(Retweet.getScreenNameComparator()) def justthree = ((topthree.size() >=3) ? topthree[0..2] : topthree[0..topthree.size()]) def jsonr = new JSONObject() jsonr.put("influencers", justthree*.toJSON()) println jsonr.toString()
In Listing 10, the response type is set to
application/json, which alerts the browser that some JSON content is forthcoming. Next, the desired
User instance is found. The parameter passed to this servlet is the user's screen name. All related
Retweet objects are then retrieved.
Using a
Comparator ensures that only unique
Retweets are returned. If a highly influential person were to retweet various tweets for a particular user, they would only be listed once. The
Comparator is shown in Listing 11:
Listing 11. A Retweet Comparator
public static Comparator<Retweet> getScreenNameComparator(){ return new Comparator<Retweet>() { @Override public int compare(Retweet arg0, Retweet arg1) { if(arg0.screenName.equals(arg1.screenName)){ return 0; }else{ return (arg0.influence > arg1.influence) ? 1 : -1; } } }; }
The
justthree variable in Listing 10 is a
List containing three
Retweet objects. Groovy's handy-dandy shortcuts let me invoke the
toJSON method on all objects within a collection with a
*. invocation.
The code results in a JSON document that looks like Listing 12 (I've added spaces and newlines for more readability):
Listing 12. A list of Retweets in JSON
{ "influencers": [ { "screenname":"stuarthalloway", "influence":2310, "date":"2010-08-17", "tweet":"Podcast w/@stuarthalloway about Clojure...", "tweetid":21451426508, "picture":"...", "username":"stuarthalloway" }, { "screenname":"aalmiray", "influence":1023, "date":"2010-08-10", "tweet":"New weekly podcast series airs today @ devWorks! @matthewmcc...", "tweetid":20812977124, "picture":"...", "username":"Andres Almiray" }, { "screenname":"wakaleo", "influence":1020, "date":"2010-09-28", "tweet":"new article published: \"MongoDB: A NoSQL datastore with (all...", "tweetid":25796403122, "picture":"...", "username":"John Ferguson Smart" } ] }
Now that I've got a servlet to pump out a list of
Retweets in JSON format, I'm ready to connect that data to my dashboard web page.
Asynchronous processing in GAE
The background processing that I set up a few sections back is pretty non-invasive from an application perspective: after a user has authenticated a session with Twitter, a job enters the queue and goes to work on obtaining a list of all retweets. My next task is to match up that application interaction with the user experience, which needs to be equally non-invasive. Rather than having a user click additional links or load additional pages, I'm going to use a little Ajax to smooth the path.
Once a user has granted my web application permission to act on his or her behalf in Twitter, they'll be forwarded to a dashboard of sorts. On the dashboard, they'll be presented with a report that will eventually list their retweets, which will be ranked by influence. When the user first clicks over to the page the report won't be ready yet, of course, so I'll have to load it up asynchronously.
Request and response
I've created a servlet to handle the asynchronous magic that returns JSON, which you saw in Listing 10 (dubbed tanalysis.groovy). Calling the servlet from a web page via Ajax isn't terribly difficult; in fact, I can simplify it with a library like JQuery. To achieve my goal of asynchronously updating the dashboard with a retweet analysis report I'm going to do a few things: first, when the page is finished loading, I'll issue an Ajax call via JQuery's
getJSON method. This method will invoke the analysis servlet from Listing 10. I'll also be passing along the current user as a parameter to the servlet call. But right before the servlet is hit, another JavaScript function will create the familiar "spinner" icon, which indicates ongoing activity.
When the servlet responds with a JSON document, the
getJSON method will update a section of the HTML DOM with a list of three retweets. Finally, another JavaScript event will fire, turning off the spinner.
Listing 13 is the Ajax code found in my dashboard.gptl file:
Listing 13. dashboard.gptl
<script type="text/javascript"> $(document).ready(function() { $.getJSON('../tanalysis', {name: "${params['tname']}" }, function(data) { $.each(data.influencers, function(){ $('.statuses').append('<li class="hentry status"> <span class="thumb" > <img width="48" height="48" src="' + this.picture + '" class="photo fn" alt="Chris Burns"></span><span class="status-body" > <span class="status-content">' + '<strong><a class="tweet-url screen-name" href="' + this.screenname + '">' + this.username + '</a></strong>' + ' Followers: ' + this.influence + '<br/><span class="entry-content">Tweet ' + '<a target="_blank" rel="nofollow" href="' + this.screenname + '/status/' + this.tweetid + '">' + this.tweetid + '</a> <img width="13" height="11" src="../images/new_window_icon.gif"/>' + ': ' + this.tweet + '</span>'+ '</span></span></li>') }); }); }); $('.log').ajaxStart(function() { $(this).text('Loading data...'); $(".statuses").text(''); $(".spinnericon").css('display', 'block'); }); $('.log').ajaxStop(function() { $(this).text(''); $(".spinnericon").css('display', 'none'); }); </script>
Note how easily the JSON response is parsed via JQuery and JavaScript. The
getJSON method basically invokes a closure with my JSON, which I've named
data. I can access elements in the JSON document quite easily — in fact, by name. In this case,
data first points to a collection,
data.influencers. I invoke the
each method on the items in the collection, and then grab individual values by it —
it.tweet, for example.
Notice the events I've defined below the
getJSON method in
Listing 13, like
ajaxStart and
ajaxStop. These methods place a spinner icon and remove it, respectively, inside of an HTML
div element.
Now I've essentially got a working web application that authenticates a user with Twitter's OAuth, grabs his or her data asynchronously, and then asynchronously updates a web page.
My only remaining concern is security.
User authentication — gotcha!
Astute readers might have noticed a slight issue with the code in Listing 13: A retweet report is generated for a user, and that
user's name is anyone on Twitter (or at least anyone for whom the web application has
data). Could an unscrupulous person start randomly passing in user names? Yes, they
could — but no report would be displayed, not unless they were logged in to
Twitter. Under the hood, I've set up a little security catch: a
ServletFilter that will check to see if the given parameter name matches the name currently in session for the current user. If there isn't a match, the request will be redirected back to the login screen.
Listing 14. User authentication with ServletFilter
package com.b50.gretweet; import java.io.IOEx UserFilter implements Filter { private ServletContext context; @Override public void destroy() {} @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { String uri = ((HttpServletRequest)req).getRequestURI(); HttpSession sess = ((HttpServletRequest)req).getSession(); String username = uri.replace("/dashboard/", ""); if(sess != null){ String sessname = (String) sess.getAttribute("screenname"); if(!sessname.equals(username)){ ((HttpServletResponse)res).sendRedirect("/"); } } chain.doFilter(req, res); } @Override public void init(FilterConfig arg0) throws ServletException { this.context = arg0.getServletContext(); } }
This security mechanism isn't beautiful, but it'll work for the purpose of my application and this article.
In conclusion
In this two-part article, I first introduced Objectify-Appengine, which is a mapping layer for GAE's datastore, much like Hibernate is for relational datastores. I showed you how to use Objectify to quickly model an application, which I then wired up with Twitter's authorization system, OAuth. I used Gaelyk to build out the application's back-end functions, like I/O and GAE queues, and JSON (rather than XML) to facilitate the browser-to-server communication. Finally, I threw in a little Ajax on the front-end, for a smoother end-user experience. I didn't really have to do much else, because GAE manages things like database management and caching.
While GAE does have some rules that you might not be accustomed to, like the 30-second request-response guideline, it also provides mechanisms for handling them, like asynchronous queues. In many ways, GAE is the perfect expression of Java development 2.0: it facilitates rapid development on the cheap while offering the scalability and reliability users have come to expect.
Download
Resources
Learn
- Java development 2.0: This dW series explores technologies that are redefining the Java development landscape, including: Objectify-Appengine (October 2010), GAE (August 2009), and Gaelyk (December 2009).
- "Max Ross on the Google App Engine" (Andrew Glover, developerWorks podcasts, November 2010): Google Staff Software Engineer (and App Engine Datastore Lead) Max Ross discusses the GAE platform, how it differs from EC2, how the datastore works, and where it's all going in the future.
- "Watch out Java, here comes JavaScript" (Neil McAllister, Fatal Exception, September 2010): Node.js and CommonJS are two promising tools advancing the notion of server-side JavaScript.
- "Mastering Ajax, Part 10: Using JSON for data transfer" (Brett McLaughlin, developerWorks, March 2007): Learn more about JSON and why it's replacing XML as the preferred format for Internet data transfer.
- "Working with jQuery, Part 1: Intermediate JQuery: Using plug-ins to create and extend the jQuery functions" (Michael Abernethy, developerWorks, February 2009): You can learn a lot about JQuery on developerWorks, starting with this introduction to useful JQuery plug-ins.
- memcache: Learn more about GAE's distributed memory cache service and API.
- "OAuth-ing Twitter with Twitter4J" (Andrew Glover, The Disco Blog, September 2010): Twitter and Twitter4J no longer allow basic authorization, which makes it a good idea to learn about OAuth.
- Browse the Java technology bookstore for books on these and other technical topics.
- developerWorks Java technology zone: Find hundreds of articles about every aspect of Java programming.
Get products and technologies.