The Duke of URL, Part 1 explains how webservers traditionally handle URLs. Now that we know this, what exactly does WebObjects do so differently?
The WebObjects Adaptor
You may have noticed that almost all webobjects URLs start with /cgi-bin/WebObjects/. The reason for this is that traditionally there would be an executable named WebObjects in the cgi-bin directory. This executable would then look to further parts of the URL to decide how to dispatch it. It's important to note that when Apache decides to run the WebObjects program it is unknown which WebObjects application supports the request. The WebObjects program itself (known as the adaptor) is compiled C code which loads quickly, having only the CGI fork/exec overhead and not the interpreter overhead. Because it is impossible to maintain state within a CGI program WebObjects instead consults a long-lived wotaskd process for all it needs to know.
Having said that, most WebObjects installations do not actually use the CGI WebObjects adaptor. Instead an Apache module mod_WebObjects is loaded into the server. Ultimately, the same exact code is executed and you can verify this because the source for the WebObjects CGI adaptor and the various web server adaptors (Apache, IIS, and others) is distributed with WebObjects. All of them share the same underlying code that consults wotaskd, the main difference being how that code is reached. For mod_WebObjects an Apache configuration directive is used to tell mod_WebObjects to trap all URLs beginning with /cgi-bin/WebObjects or actually /<anything>/WebObjects. For the CGI WebObjects adaptor the URL is trapped by virtue of the CGI adaptor being a file named WebObjects in the cgi-bin (or other) directory.
The common adaptor dispatch code starts by examining the path component after /cgi-bin/WebObjects which should look like SomeApplication.woa such that the beginning of the URL is /cgi-bin/WebObjects/SomeApplication.woa. At this point the WebObjects program knows that it needs to look for a running application named SomeApplication. Before it does this it checks to see if the next component of the URL is an integer. If it is an integer then it uses it as an application instance number. If it is anything other than an integer then the WebObjects request dispatcher ignores it.
An application instance is one running copy of your Java code uniquely identified by its application name and instance number. The application instance is a separate process from the webserver and may be running on either the same machine or a different machine. Before any requests can be handed for a given application name at least one application instance process must have been started and have checked in with the wotaskd service. To ensure requests don't get stuck too long waiting for an instance the wotaskd service periodically checks all attached applications by sending them a "heartbeat" request. This way if a particular instance has hung the wotaskd can simply use another available instance.
As mentioned, the WebObjects adaptor also speaks to wotaskd on port 1085 and uses it for its configuration state. To determine which application process must be contacted to service the request it sends the application name and instance number to wotaskd. The wotaskd responds with the hostname and port number of the application. Typically the first instance of the first application configured on a machine will be running on TCP port 2001.
Notice that the WebObjects adaptor only looks as far as the application name and instance number, if any. Everything thereafter is ignored so the decision as to what to do with it rests entirely in the application instance that is contacted to service the request.
What that means is that unlike PHP or ASP.NET which only see requests for their .php or .aspx files, a WebObjects application sees all requests beginning with /cgi-bin/WebObects/SomeApplication.woa.
WebObject Application Request Dispatch
As we discussed in Part 1 regarding IIS 7's integrated pipeline, the ability to handle arbitrary URLs is only as good as what you can do with them. In the case of WebObjects the answer is basically everything; in particular any arbitrary component may be instantiated and used to generate the response. The reason for this is that the underlying pages (embodied by .wo bundles and their associated Java classes) are completely decoupled from the request URLs. It is entirely up to the application's dispatch code to decide which page (i.e. .wo component) is to be used to generate output for the user.
As with ASP.NET the first code providing an opportunity to override the request handling is in your Application singleton. The main interesting method is WOApplication.dispatchRequest(). This function takes a WORequest and returns a WOResponse. Although it is possible to override this method I wouldn't recommend it because WOApplication provides an alternate method for installing custom URL handlers.
The registerRequestHandler() method takes an instance of a WORequestHandler and a String and registers the request handler by name with the application dispatcher. The WebObjects framework by default registers a few built-in request handlers and only a fairly advanced WebObjects coder would ever register additional request handlers.
The two most common user-visible request handlers are an instance of WODirectActionRequestHandler registered as "wa" and an instance of WOComponentRequestHandler registered as "wo". So any WORequestHandler beginning with /cgi-bin/WebObjects/SomeApplication.woa/1/wa/ is sent to the WODirectActionRequestHandler instance by calling its handleRequest() method. The format of this is exactly the same as WOApplication.dispatchRequest() which is accept a WORequest, return a WOResponse.
If you are familiar with ASP.NET MVC then you will see a lot of parallels between MVC and DA. The primary difference is that MVC requires you to register several pieces of information with the MVC dispatcher: a regex pattern to match the URL, a controller class, a method name to call on that controller class, and a .aspx page to use as its view.
On the contrary direct actions are not registered. A direct action class derived from WODirectAction is looked up via reflection and instantiated with a WORequest. Then its performActionNamed() method is called with the action name and WODirectAction implements this to find and call a method of the form <actionname>Action taking no arguments.
The direct action request handler takes URLs of two forms: .../wa/actionname and .../wa/class/actionname. The first form looks for a class named DirectAction which is your class (not a framework class). A new WebObjects application is created by the development environment with a skeleton DirectAction class implementing defaultAction() to return pageWithName("Main"). The second URL form looks for the named class. Because most people like to see all lowercase URLs but capitalized class names the request handler capitalizes the first letter such that wa/foo/bar looks for a class named Foo not a class named foo. Method names in Java are conventionally non-capitalized camelCase (as opposed to class names which are usually capitalized CamelCase), so no recapitalization is necessary to find barAction() given "bar".
The *Action() method returns any object implementing WOActionResults. The WOActionResults interface defines one single method: generateResponse() returning a WOResponse. The interesting thing is that WOResponse itself implements WOActionResults.generateResponse() to simply return itself. And WOComponent implements it to create a new WOResponse object and call appendToResponse() on itself. What this means is that in a WOComponent the response object is not actually available until it is time to generate the response.
In the case of actions (which we're talking about here) the request flow on the page is actually a bit short circuited. Recall that the request came in to the WODirectAction-derived class and not directly to a page. This means it is up to the action handling code to deal with any GET or POST values and other aspects of the request.
If the action method is going to be using a WOComponent to generate the response it will call its own pageWithName() method (defined in the WOAction class which is a base class of WODirectAction). This method will ask the application to create a new instance of the WOComponent-derived Java class with the given name. The code might look like this:
WOComponent nextPage = pageWithName("DisplayBlogEntriesPage");
From your action code you might cast the returned value to the appropriate Java class (i.e. DisplayBlogPostsPage is what you are looking at now) and call methods like setBlogEntriesDataSource() or setBlogEntriesQualifier() on it. Alternatively you might take advantage of key-value coding, leave it as a WOComponent and instead call takeValueForKey(..., "blogEntriesDataSource") on it. Finally you return the page which will cause it to be generated and returned to the user. In code that would look something like this:
DisplayBlogEntriesPage nextPage = (DisplayBlogEntriesPage)pageWithName("DisplayBlogEntriesPage"); String requestedYear = context().request().stringFormValueForKey("year"); nextPage.setBlogEntriesQualifier(BlogEntry.qualifierToMatchYear(Integer.valueOf(requestedYear))); return nextPage;
Instead of returning the page and letting the base handler call generateResponse() on it to get the WORespone object you might instead wish to return nextPage.generateResponse(). In this case you will be returning the actual WOResponse object which can have advantages. The main advantage is that if your page throws while generating its content you have an opportunity to catch it at this point and return some completely custom response. That said, if you simply return the component itself as the action result the underlying code will call generateResponse() on it taking care to also handle the exception and returning a generic exception page.
Recall that I said page flow is short-circuited from full page flow. Ordinary page flow is takeValuesFromRequest(), invokeAction(), appendToResponse(). But since this is a direct action there is no existing page postback data to be restored in takeValuesFromRequest() nor any decision to be made as to which page is next in invokeAction(). So processing begins and ends with appendToResponse().
So now you are probably wondering how one would actually implement a web form if the page doesn't get a chance to see the request. The answer is that you don't. Accepting the form values from the user is something that occurs in takeValuesFromRequest() which is only valid in the context (that's the WOContext) that created the form. To handle form postbacks, WebObjects uses the content request handler under the "wo" request handler key.
The Component Request Handler
The component request handler serves mainly to handle postback data. Regardless of which WOForm on which WOComponent is causing the postback the form submission URL will be for the WOComponentRequestHandler. Now some people find this offensive because you wind up with URLs that look like /cgi-bin/WebObjects/WOBlog.woa/1/wo/1mtIbBwW99kJt9QyXGccqg/6.PageWrapper.18.104.22.168.22.214.171.124.1.1. Wow, that's user friendly... not!
In actual practice once a user begins to fill out and submit a form it very rarely matters which URL appears after the form is submitted. That said, it's certainly possible, after having processed the postback data, to send the client a redirect to another page.
If you were implementing edit of a customer record from a direct action you might begin this by taking the user to .../wa/customers/edit?customerid=123. As we know from the direct actions section this causes a Customers object derived from WODirectAction to be instantiated with the request and its editAction() to be invoked. The editAction() method might then do this:
WOComponent nextPage = pageWithName("EditCustomerPage"); Customer customer = Customer.fetchCustomerByID(context().request().stringFormValueForKey("customerid"); nextPage.takeValueForKey(customer, "customer"); return nextPage;
Because the request URL was a direct action the user will see the more friendly direct action URL but the EditCustomerPage form. When he clicks a button (say a Save button) the form is submitted to one of the long component request handler URLs. From there a method of EditCustomerPage named perhaps saveAction() will save the changes to the database and return a redirect response to redirect the user back to .../wa/customers/list. This way the user never sees the component URL. If that's what you want of course.
The saveAction() does not have to return a redirect response. If saving failed it will likely return null which tells WebObjects to redisplay the page. Generally one would do something like set an errorString instance variable to some error text and have a WOString on the page which outputs this text to the user if it is present (probably inside of a WOConditional so nothing is output if errorString is null). In this case the user will see the long component URL because he is not being redirected back to the edit direct action but instead the new page content (including the error string) is directly returned.
If these URLs are undesirable to you there is actually a simple way around this for modern browsers. Instead of generating a full postback you can use an AJAX update panel. In this case the page is updated by the browser in situ so the URL doesn't change. The underlying AJAX URL will be a gnarly component-style URL (though as of WebObjects 5.4 it goes through a separate AJAX request handler).
From a coding perspective the code is the same. The AJAX handler invokes takeValuesFromRequest, then invokeAction, and the *Action() method returns null to indicate the page should be redisplayed. But instead of regenerating the entire page the code instead calls appendToResponse on only the portion of the element tree under the panel that is being updated. If something other than null is returned then the client will be sent instructions to instead retrieve an entirely new page.
The interesting thing about this is that if the browser lacks AJAX it still works. The only difference is that the button click returns an entirely new page with the special component URL. All page state is fully maintained because the component handler is able to restore the exact context using the URL to tell it which context to restore.
If you are used to ASP.NET you will notice how we've been talking about the potential for "returning" a redirect as opposed to "performing" a redirect. This is because at invokeAction() time the response object has not been instantiated. If we don't mind the user seeing a component action URL we can actually instantiate and return an entirely different page! For example, we might do something like this:
ListCustomersPage listPage = pageWithName("ListCustomersPage"); listPage.ensureCustomerIsVisible(this.customer); return listPage;
In this fashion if the list page has a pager and the customer we just finished editing is on say page 250 of 300 we can pass the actual customer object to the list page and it can do the work of figuring out which page needs to be displayed for that customer to appear in the list. It might also highlight the row for that particular customer so the user can clearly see which customer he just finished editing.
At this point you begin to realize the inherent freedom that the component request handler provides you. At the expense of some pretty gnarly URLs you are afforded the opportunity to drag the user anywhere you wish. The URL becomes simply a vehicle for maintaining state across each request. Best of all, a certain number of URLs (typically 30) are remembered internally by the request handler. This means that back/forward by the client actually work and despite the fact that the client may repost the same data the server will actually be intelligent enough to return the response from its cache. So if the user decides to back track after placing his order, the order will not be submitted again. Ditto for if the user is dumb and likes to double-click submit buttons. The first click posts all the form data and begins generating the response. But before the response can be sent to the client the client posts the form a second time. No problem! The second request realizes that a response has already been generated or is waiting to finish generating. This response is cached so it is simply sent to the user without reinvoking any of your action code.
For an application based on user actions, particularly the typical CRUD (Create, Read, Update, Delete) the user's navigation expectations can be easily met because it is possible to directly instantiate new pages and even inform them of exactly where to return to when finished. And I don't mean passing a value via a query string or a session variable. I mean actually passing the current page instance to the next page instance so the user can be returned to exactly the page he came from instead of to whatever page the developer thought would probably be best to take the user to when he's finished editing.
A very simple example is an admin tool for a web-based store. The clerk may list customers then pick a customer to edit. When she is finished she would expect to go back to the list of customers and be looking at the same page. But she might instead be viewing a particular order and need to change some customer information. She clicks the edit link on the customer, edits the data, saves the customer and goes back to which page? The sensible thing is to return her to the order page. Most other environments cannot easily record this information so in a good majority of web apps she's taken back to a list of customers because that's where the developer assumed it would probably be best to take her when she's done editing a customer.
Component actions are so powerful that it's even possible for a CRUD site to be generated at runtime from a database model. I don't mean going in and running some wizard to generate editCustomer.aspx, listCustomer.aspx, editOrder.aspx, listOrder.apsx. I mean having one EditPage.wo that consults configuration data to determine which attributes to display and which components (e.g. text box, check box, radio buttons, pop-up lists) to display for each attribute.
The ability to do this is already a part of WebObjects and is known as Direct To Web (D2W). Let's face it, at some point every app needs a CRUD section, if only to be used as an admin interface. Would you rather write a bunch of list and edit pages to enter your blog entries or would you rather model your data once and let D2W generate your admin interface for you?
What About Content?
Component actions are all well and good for web-based user interfaces. In my opinion they absolutely exceed the abilities of every other web development environment out there. But for serving dynamic content component actions are simply inappropriate.
We did cover direct actions and these can be used to serve dynamic content. For instance you might make a direct action for every user-visible page on your site. Perhaps you'd have .../wa/sections/products, .../wa/sections/aboutus, .../wa/sections/services or whatever.
It's workable and because they are direct actions they are regular URLs (albeit rather long ones). With URL rewriting in the webserver you could perhaps shorten these to /products, /services, /aboutus. But in the end you wind up having to create a productsAction, servicesAction, and aboutUsAction in a Sections WODirectAction class.
In Part 3 we'll explore an enhancement to WO that will make you completely rethink the way you serve a content-managed site.