Editor's note: This is the third installment in our ongoing series covering WebObjects (WO). In the first article, "An Introduction to WebObjects," Josh Paul showed you how WO consists of a set of frameworks that allow you to write cross-platform, server-distributed applications. Then, in the second tutorial, "Using WebObjects "Direct to Web" Technology," Josh shows you how to create a full-blown web application in just a few simple steps. This week, he picks up where he left off and shows you more tips and techniques for setting up Direct to Web (D2W) applications.
Hey, hey! Welcome back. If you're just joining us, we're working with Apple's WebObjects software. That'd be WO for short, which is what you should be saying at some point, if you haven't already. For reference, I located Steve Jobs' WWDC announcement on the $49,301 price drop. That's right, WO once was affordable only for large enterprise operations, such as the Apple Store. But now it's within the reach of anyone who wants to install and implement it.
In the last article, you stepped through a simple Direct to Web application. I'd like to continue on that path, and since I'm writing this before you read it, you have no choice. So I'm going to start walking ...
Yup, I'm back to the Login Page thing. Since I left you hanging out to dry in the last article, I figured I'd integrate a little code. Open your old project, or create a new one, following the steps from the previous article. In the next few steps you are going to implement a better login process.
Locate your Main.java file within your project. You're going to add a method to see if the client has entered a valid username and password. It'll be painless, I promise.
By using the exceptionally useful EnterpriseObjects Frameworks (EOF), you'll be able to compare the client-provided information with the information in your database. You'll be using the specific classes of EOUtilities and EOEditingContext. The EOEditingContext, to quote Apple, " manages a graph of enterprise objects in an application; this object graph represents an internally consistent view of one or more external stores (most often a database)." For now, you can consider it a playground for your data, where you can invite objects in to play and to be played with.
Since the EO-based classes are not always expected in a WO component, you'll need to import them.
import com.webobjects.eocontrol.*;
import com.webobjects.eoaccess.EOUtilities;
import com.webobjects.eoaccess.EOObjectNotAvailableException;
In order to provide usable feedback to the client, you should also add a String variable. I called mine feedback.
public String feedback;
You can then add your new method.
public EOEnterpriseObject userForCredentials() {
EOEnterpriseObject aUser;
// the magical EOEditingContext..
EOEditingContext ec;
// use with EOUtilities to query the database
NSDictionary bindings;
// used to determine which columns to query
Object[] keys = {"username", "password"};
Object[] values = {username, password}; // used to query the database rows
if ((username == null) || (username.length() == 0) ||
(password == null) || (password.length() == 0)) {
feedback = "You must provide both a username and a password.";
return null;
}
aUser = null;
ec = session().defaultEditingContext();
bindings = new NSDictionary(values, keys);
try {
aUser = EOUtilities.objectMatchingValues(ec, "User", bindings);
}
catch (EOUtilities.MoreThanOneException e1) {
System.err.println("ERROR: More than one User with username '" + username +
"' and password '" + password + "'.");
feedback = "Please contact a System Administrator.";
}
catch (EOObjectNotAvailableException e2) {
feedback = "Your Login information was incorrect.";
}
return aUser;
}
Obviously, unless you call your new method somehow, it's useless. Locate your defaultPage method and make some magic.
public WOComponent defaultPage() {
if (isAssistantCheckboxVisible()) {
D2W.factory().setWebAssistantEnabled(wantsWebAssistant);
}
if (userForCredentials() != null)
return D2W.factory().defaultPage(session());
return null;
}
If you're curious how the heck defaultPage gets called, open your Main.wo file. (If you didn't just open the Main.wo file, please do so now.) The WebObjects Builder application should launch and present the component to you.
Within the Login Form, there is a WOActiveImage displaying the Login button. If you select the Login button, you'll notice in the Inspector window a binding called action, which is bound to your defaultPage method. When a client clicks the button, the defaultPage method is called. Therefore, you can safely assume your userForCredentials method will now be called as well.
While you have the Main.wo file open, you should add a WOString and bind it to your feedback variable. This will allow you to provide your clients with information, such as "Your login information was incorrect." When you've added the WOString and bound it, your Login Form should look something like this:
If you'd like, you can change the format of your feedback. I will usually either italicize or bold, as well as add some color (like red).
You should now be able to build and run your application. Should you run into problems, or if you're curious what EOF is doing, you can enable some simple logging by adding the following to your Properties file: EOAdaptorDebugEnabled=true. Upon doing so and running your application, you'll be able to see what SQL is being used, in addition to what JDBC driver is being used.
Okay. Now on to some more interesting topics.
From within your project, open your EOModel file. It will most likely be listed under the Resources Group. Opening the EOModel should launch the EOModeler application.
In your Toolbar, you will find a button to generate Java class files for your Entities. If you've been following along with the past articles, you should only have one Entity named User. It should be configured similar to this:

If you wanted to present a User's first and last name, there are a number of ways you could accomplish it. You could create a calculated column in your database, use WebObjects Builder to create a component with two WOStrings, use EOModeler's derived attribute ability, or choose any number of other options. In the next few paragraphs, you're going to create a User class and add a convenience method for nameFull.
Select your User Entity and click the Generate Java button
located in the Toolbar. You will (most likely) be presented with an Alert informing you about your User class needing a class name. You can safely click the default button, Use User. However, do not save the file yet!

When your are presented with the Save Dialog box, rename your file something like "_User.java" or "User_core.java," as you will be implementing the Generation Gap pattern. I chose _User.java. Following this pattern may take a few extra steps, but has been well worth the effort, in my experience. Upon saving, you will be asked if you want to insert the file in your project. You do.
When you return to using ProjectBuilder/Xcode, you'll discover that your _User.java file has been placed into your project, most likely under the Classes group. Select the file and edit it to remove the wrench you've thrown into the system by matching the class name with the file name.
public class _User extends EOGenericRecord {
public _User() {
super();
}
Next, you should create a new file. When presented with the Assistant, select the WebObjects Java class option. You should name your new class "User.java" and select the Application Server as the Target.

In order to implement the pattern, you'll simply inherit from _User.
import com.webobjects.foundation.*;
import com.webobjects.eocontrol.*;
import java.math.BigDecimal;
import java.util.*;
public class User extends _User {
public User() {
super();
}
}
Feel free to browse the code EOModeler generated for you. You should notice a very simple, yet exceptionally powerful, pattern. Specifically, the storedValueForKey and takeStoredValueForKey methods. Sure enough: Key-Value Coding (KVC). I am not going to dive into KVC here; sorry to dash your hopes, I just wanted to point it out.
From this point forward, whenever you use EOModeler to generate Java for your User Entity, you can safely assume any custom logic you've implemented won't be overwritten. However, you will have to rename the generated file and "remove the wrench" every time.
So how do we determine a User's full name? Well, personally I'm partial to a First Last order, so that's how I implemented my solution. Feel free to come up with your own.
public String nameFull() {
return nameFirst() + " " + nameLast();
}
Now what?
|
You should now be able to build and run your application. When you receive the Login form, try:
Username or Password fields empty.When you decide to log in correctly, make sure you select the Assistant checkbox. Once you have received the Home Page, click on the Customize button. The D2WAssistant should launch in a new window. You then want to customize a List page for a User; a simple way to do so is to simply query for Users.

The D2WAssistant will then update to allow you to edit the current Rule(s). You should be able to select nameFull from the list of Properties. If you decide to Show nameFull, either click the Update or Save buttons to see your change reflected on the current page.
You'll notice D2W is displaying the property nameFull without prettifying it. You can change the display by simply selecting nameFull under the Show list and entering Full Name in the Display field:

But there's a more "global" way to accomplish it. For now, enter Full Name and save the changes.
Although you can run the D2WAssistant while editing your .d2wmodel file, I choose not to in order to avoid overwriting or corrupting the file. So, to be safe, log out of your application. Locate and open the user.d2wmodel file in your project. You should now be editing your file using the Rule Editor application.
Since you'll probably want to display something other than a label like nameFull throughout your application, you can create a more generic Rule. To do so, simply select your new Rule, which has the Left-Hand Side argument of:
((task = 'list') and (entity.name = 'User') and (propertyKey = 'nameFull'))
Remove the first argument: (task = 'list').
By simply removing the task argument, you've just enabled D2W to always display the Full Name label any time it needs to display a User's nameFull. In other words, any time a web page is created and that web page is going to display information obtained using a User's nameFull method, it will be labeled as Full Name. That's it ... I tried ... I don't know how else to explain it ... because I feel like I'm going in circles ... (Paragraph courtesy of Red Bull.)
Your Rule should now look something like this:

If you wanted to have a really generic Rule, you could even remove the entity.name = 'User' argument. You would then enable D2W to always display the label Full Name for every nameFull property. So, in the future, if you had a City class that had a nameFull property, D2W would implement this Rule just as it does for a User.
Obviously, your User is pretty light on details. What if you wanted to contact a User? How would you do so? I'm partial to email.
Return to EOModeler and select your User Entity. You're not going to do anything too fancy here, just add another attribute and name it email. I took the lazy person's route and did a copy/paste using the username attribute, and simply renamed both the name and column.
Since you've now changed the schema of your User, you'll need to synchronize your EOModel and your database. As I discussed in the last article, how you synchronize your EOModel and your database is up to you. Please be aware you've now implemented a Login panel, so if you drop your table, you'll need to add a User to your database somehow.
Once you've made the change, go ahead and generate the Java file for your User again.
User Entity.I realize these steps are fairly tedious for an application of this size, but you shouldn't have to change the schema of you model(s) too often.
Your nameFull method should have survived this minor onslaught. As your application grows, and the logic for each of your EOEnterpriseObjects becomes more complex, the Generation Gap pattern should prove quite beneficial. At the very least, you'll have a clean separation between code "owned" by EOModeler and code "owned" by you (or your employer).
Build and Run your application again. When logging in, select the Assistant checkbox. Choose to Customize, wait for the D2WAssistant to launch and then query for a User. You should ultimately receive a List page for a User.
In the D2WAssistant window, notice the email property is now available. Go ahead and select it and add it to the Show list. Directly to the right of the Show list is a popup button. This popup button will allow you to create a Rule to assign a reusable component, given these criteria (i.e., a List page, a User, and the email property). When you click on the popup button, you should be able to select a component named D2WDisplayMailTo.

By selecting this component, whenever a List page for a User needs to display the email property, it will be presented as a mailto: link. Therefore, clients will be able to simply click on a User's email address in order to compose and send email.
While still viewing the List page, you will be able to edit a User by clicking the Edit button
. The D2WAssistant should update to reflect the fact you are now viewing an Edit page. Again, select the email property and add it to the Show list.

When you click on the Save button, in the D2WAssistant, you should notice your web page update. You can now edit a User's email address. Enter an appropriate address and click the Save button on the web page. You should then receive a List page.
Now, when viewing a List of Users you'll be able to send them an email with a simple mouse click.

When the address is clicked, your Mail application should launch and create an email addressed to the selected User.
If you'd like, you can refactor your Login code (using Move Method) from Main.java to User.java. When doing so, you might wind up with a static method in your User class. In fact, your User class might look something like:
public static EOEnterpriseObject userForCredentials
(EOEditingContext ec, String uid, String pwd)
throws EOObjectNotAvailableException {
EOEnterpriseObject aUser;
// use with EOUtilities to query the database
NSDictionary bindings;
// used to determine which columns to query
Object[] keys = {"username", "password"};
// used to query the database rows
Object[] values = {uid, pwd};
if ((uid == null) || (uid.length() == 0) ||
(pwd == null) || (pwd.length() == 0)) {
throw new EOObjectNotAvailableException
("Username and Password required.");
}
aUser = null;
bindings = new NSDictionary(values, keys);
try {
aUser = EOUtilities.objectMatchingValues(ec, "User", bindings);
}
catch (EOUtilities.MoreThanOneException e1) {
System.err.println("ERROR: More than one User with username '" + uid +
"' and password '" + pwd + "'.");
throw new EOObjectNotAvailableException
("Please contact a System Administrator.");
}
catch (EOObjectNotAvailableException e2) {
throw new EOObjectNotAvailableException
("Your Login information was incorrect.");
}
return aUser;
}
}
I will leave the Login implementation in Main.java to you.
Josh Paul is the founder and CEO of Aweli, a startup focused on digital video solutions, and the author of Digital Video Hacks. He has provided software and service solutions to entertainment production companies throughout Los Angeles and New York.
Return to Mac DevCenter
Copyright © 2009 O'Reilly Media, Inc.