Selecting a subset of Objects in a WORepetition

Often you need to allow a user to select a subset of items from a WORepetition to be able perform an action on them. This example will do that for you:

public Object item; // The "item" binding on your WORepetition
public NSMutableArray selections = new NSMutableArray();
 
public void setSelectedItem(boolean selected) {
    boolean hasItem = selections.containsObject(item);
    if (selected) {
        if (! hasItem) {
            selections.addObject(item);
        }
    } else if (hasItem) {
        selections.removeObject(item);
    }
}
	
public boolean selectedItem() {
    return selections.containsObject(item);
}

Add a WOCheckbox to your WORepetition and bind its checked binding to selectedItem. Any item selected will be added to the selections array when the form is submitted.

Localizing DB content with valueForKeyPath();

It is fairly straight forward to localize a WebObjects application using either localized components or localized strings as I wrote about here. But things get a little more complex when you need to localize content coming from a database.

Usually multilingual database content is modeled something like this:

product_model.gif

Regardless of your model though, your task is usually to identify the correct object from an array based on its language key. You might do that in your Product EO:

public ProductDescription englishDescription() {
   NSArray descriptions = CBArrayUtilities.filteredArrayWithKeyValueEqual(
						descriptions(),
						"language",
						"English");
   if (descriptions != null && descriptions.count() > 0 ) {
      return descriptions.lastObject();
   }
   return null;
}

Note: The source for the filteredArrayWithKeyValueEqual method is here

Or you might move that logic into your components. Either way these kinds of solutions tend to result in:

  1. Lots of code in lots of places (Entities, Components, etc.).
  2. Adding supported languages require massive code changes. New accessors, component level changes, etc… yuck.

I prefer a more flexible approach. I like to get my components to recognize a new @localized key path operator by overriding their valueForKeyPath() method. Since my components usually inherit from a custom WOComponent subclass (ie. CBLocalizedComponent) this is easy to do. This example code shows one way you might do this:

/*
* Overriding WOComponent's valueForKeyPath method to 
* get it to recognize the @localized operator.
*/ 
public Object valueForKeyPath(String keypath) {
   String localizedOperator = "@localized";
   int locOpLength = localizedOperator.length();
   String sourceKey, valueKey;
   int index = keypath.indexOf(localizedOperator);
   if (index > -1) { 
      /*
       * If the index of the localizedOperator is > -1 then it is contained
       * in the keyPath, and we should proceed.
       * First, extract the parts of the keypath before and after the 
       * localizedOperator
       */
      sourceKey = keypath.substring(0, index - 1);
      valueKey = keypath.substring(index + locOpLength + 1, keypath.length());
      /*
       * Use the sourceKey to give us our array of objects
       */
      NSArray source = (NSArray)this.valueForKeyPath(sourceKey);
      /*
       * Filter the source array for the current language, we're assuming 
       * it only contains one object so get it.
       */
      NSArray filtered = (NSArray)this.filteredArrayForCurrentLanguage(source);
      NSKeyValueCodingAdditions object = 
		(NSKeyValueCodingAdditions)filtered.lastObject();
      if (object != null) {
         if (valueKey != null &&  valueKey.length() > 0) {
            /*
             * If the object and the valueKey isn't null, use the valueKey 
             * to get the requested value
             */
            return object.valueForKeyPath(valueKey);
         }
         /* 
          * Otherwise, return the object
          */
         return object;
      }
      return null;
   }
   /*
    * If the index of the localizedOperator is -1 then we can just
    * let super deal with it
    */
   return super.valueForKeyPath(keypath);
}
 
/*
* Utility method to filter an array returning the objects that match the
* current selected language. If no objects exist for that language return
* the default language.
* 
* It is assumed that the objects contained in the array will all implement
* a "language" attribute.
*/  
public NSArray filteredArrayForCurrentLanguage(NSArray array) {
   NSArray filteredArray = new NSArray();
   NSArray availableLanguages = (NSArray)array.valueForKeyPath("language");
   String currentLanguage = ((Session)session()).currentSelectedLanguage();
   if (availableLanguages.containsObject(currentLanguage)) {
      filteredArray = CBArrayUtilities.filteredArrayWithKeyValueEqual(array, 
							"language", 
							currentLanguage);
   } else {
      String defaultLanguage = Session.DEFAULTLANGUAGE;
      filteredArray = CBArrayUtilities.filteredArrayWithKeyValueEqual(array, 
							"language", 
							defaultLanguage);
   }
   return filteredArray;
}

The code in the WOComponents rely on some values being available from the Session. This is an example of that code:

protected NSArray _requestedLanguages;
protected String _currentSelectedLanguage;
public static String DEFAULTLANGUAGE = "English";
 
/* 
 * Get the array of requested languages from the browser request. 
 * I'm using ERXSession, ERContext, and ERXRequest from ProjectWONDER
 * The ERXRequest appends a "NonLocalized" value at the end of this array
 * that will not exist if you use the standard WORequest. You will need
 * modify the code accordingly.
 */
public NSArray requestedLanguages() {
   if (_requestedLanguages == null) {
      _requestedLanguages = this.context().request().browserLanguages();
   }
   return _requestedLanguages;
}
   
public void setRequestedLanguages(NSArray array) {
   _requestedLanguages = array;
}
  
/*
 * Identify the first requestedLanguage that matches the available languages
 * for this application. If none of them match, use the default language.
 * For performance reasons we cache this once per Session.
 *
 * Notes:
 * The ERXSession.availableLanguagesForTheApplication() returns an array
 * of the languages currently supported by the application. If you are not
 * using ProjectWONDER:
 *    - You will need to do identify the languages your application supports 
 *    - You will need to handle the "NonLocalized" browser request differently.
 */
public String currentSelectedLanguage() {
   if (_currentSelectedLanguage == null) {
      int rlc = requestedLanguages().count();
      for (int i = 0; i < rlc; i ++ ) {
         String lang = (String)requestedLanguages().objectAtIndex(i);
         if (lang.equals("Nonlocalized")) {
            _currentSelectedLanguage = DEFAULTLANGUAGE;
            return _currentSelectedLanguage;
         } else if (availableLanguagesForTheApplication().containsObject(lang)){
            _currentSelectedLanguage = lang;
            return _currentSelectedLanguage;
         }
      }
      _currentSelectedLanguage = DEFAULTLANGUAGE;
   }
   return _currentSelectedLanguage;
}
       
public void setCurrentSelectedLanguage(String value) {
   _currentSelectedLanguage = value;
}

With this code in place we can bind values in our WOComponants with key paths that look something like this:

eman.dezilacolnull@.snoitpircsed.tcudorp

Our modified components will recognize the @localized operator, and return only the description that matches the current selected language. We can easily add additional languages to our application without having to change any of the code in any of our WOComponents or EOs, and new EOs in our model only need to implement a language attribute to support full localization.

Note: Like any example code, this shows only one possible way you might chose to implement this. Feel free to use it as a starting point. The code was tested before I started marking it up, if you find any errors please let me know. No warrantee implied, blah, blah, blah. 🙂

Zen and the art of KVC

Key Value Coding – it’s one of the reasons WebObjects is so much fun.

The Basics

For those uninitiated, KVC allows your code to access method, attributes, or variables in your classes using string keys. When you are binding to a WOElement like WOString in WebObjects Builder you are using KVC (WOString, in-case you didn’t know, is a "String" with "WO"… in-joke, sorry). But KVC can help you a lot in your own code.

For instance, this:

public String myFirstName() {
	return person.firstName();
}

can be re-written as this:

Person person; // assume exists and implements NSKeyValueCoding
  
public String myFirstName() {
	return (String)person.valueForKey("firstName");
}

You can also set the value on an object using:

public String setMyFirstName(String newName) {
	person.takeValueForKey(newName, "firstName");
}

Why bother? Well for one thing, when using KVC you don’t need to know the type of the object. So this code would work just as well:

EOEnterpriseObject eo; // assume exists
  
public String myFirstName() {
	return (String)eo.valueForKey("firstName");
}

Meaning the eo could be a Person, or an Employee, or a Customer, or any other object that returns a value for firstName. This becomes very useful when you are writing reusable objects, handling conditional navigation in your components (ie: when the destination component will not be determined until run-time), or creating bindings for reusable components.

How does WebObjects find a match for your key? The KVC implementation in WebObjects will match using these rules:

  • When "getting" values:
    1. a method named firstName or getFirstName
    2. a variable named firstName or _firstName
  • When "setting" values:
    1. a method named setFirstName
    2. a variable named firstName or _firstName

Based on these rules, I like to name my accessors: varName() and their variables: _varName so there is no confusion about which will be hit by KVC.

Following the Path

In addition to valueForKey and takeValueForKey, you can access values through their paths using valueForKeyPath and takeValueForKeyPath. Lets say that you have an order that has an approver who has an address. You could access the address like this:

Order order; // assume exists
  
public String approverAddress() {
	return (Address)order.valueForKeyPath("approver.address");
}

If you were in a WOComponent or some other object that implemented the NSKeyValueCoding interface, you could use code like this:

public String approverAddress() {
	return (Address)this.valueForKeyPath(
			"order.approver.address");
}

This example demonstrates why KVC takes me to my happy place: If any of the objects in the path are optional, KVC will gracefully handle the nulls. So the code above could potentially replace:

public String approverAddress() {
	if (order != null) {
		Person approver = order.approver();
		if (approver != null) {
			return approver.address();
		}
	}
	return null;
}

A Strong Foundation

Key Value Coding is implemented in many of the Foundation classes like NSArray, NSDictionary, etc. As an example, if you have an NSArray called orders that contains order objects that all need to be canceled by setting their cancelFlag to true. You could use:

NSArray orders; // assume exists
  
public String cancelOrders() {
	orders.takeValueForKey(Boolean.TRUE, "cancelFlag");
}

Couple this with valueForKeyPath and takeValueForKeyPath you have a very powerful set of additional tools at your disposal.

Putting it all together

Here is a final example using KVC and the NSArray operators from ProjectWONDER that I mentioned previously here.

Perhaps we have this:

order <-->> product <-->> sku

We want to get all of the skus. Each order will have several products but not all products will have skus. Using traditional java we’d end up having to do something like this:

public NSArray allSkus() {
	NSArray products = order.products();
	int productCount = products.count();
	NSMutableArray allSkus = new NSMutableArray();
	for (int i = 0; i < productCount; i ++) {
		Product product = products.objectAtIndex(i);
		NSArray theseSkus = product.skus();
		if (theseSkus != null && theseSkus.count() > 0) {
			allSkus.addObjectsFromArray(theseSkus);
		}
	}
	return allSKus;
}

Using KVC and the @flatten opperator from ProjectWONDER we can do something like this:

public NSArray allSkus() {
	return (NSArray)order.valueForKeyPath(
				"products.skus.@flatten");
}

First we ask the order for its products, then we ask the products for their skus. KVC will discard any null results, and we end up with an NSArray of NSArrays of skus. The final @flatten operator will flatten this nested array into a single NSArray of skus.

Want the result sorted by skuNumber? No problem:

public NSArray allSkus() {
	NSArray allSkus = (NSArray)order.valueForKeyPath(
				"products.skus.@flatten");
	return (NSArray) allSkus.valueForKeyPath(
				"@sortAsc.skuNumber");
}

Embracing Key Value Coding can reduce the code you write and make your code more robust. It can also help you work the "WebObjects Way".

I highly recommend it.

Note: If you feel uncomfortable implementing all of ProjectWONDER just to get the NSArray operators, don’t. Just add the ERXArrayUtilties.java file to your project and call ERXArrayUtilties.initialize(); in your Application’s constructor.

Programatically creating a WODisplayGroup (take 2)

I wrote about programatically creating WODisplayGroups here. In that example I use a find() method to explicitly do a fetch, and those objects were assigned to the the WODisplayGroup.objectArray().

That is just my preferred way of handling the object fetch, but of course it is not the only way. If you wanted to emulate what happens if you create a WODisplayGroup by dragging an Entity from EOModeler into WebObjects Builder, you might do something like this:

protected WODisplayGroup _displayGroup;
protected EOEditingContext ec;   //Assume exists
protected String dateSourceName; //Name of the Entity
 
public WODisplayGroup displayGroup()
{
    if (_displayGroup == null) {
        _displayGroup = new WODisplayGroup();
        _displayGroup.setNumberOfObjectsPerBatch(batchSize);
        EODatabaseDataSource ds = new EODatabaseDataSource(
					ec, 
					dataSourceName);
        _displayGroup.setDataSource(ds);
        _displayGroup.fetch();
    }
    return _displayGroup;
}

You could also create a WODisplayGroup based on a master detail relationship like this:

protected WODisplayGroup _displayGroup;
protected EOEnterpriseObject masterObject; //The Master object
protected String detailKey; //The name of the relationship 
                            //in the masterObject that will
                            //provide the _displayGroup's objects
 
public WODisplayGroup displayGroup()
{
    if (_displayGroup == null) {
        _displayGroup = new WODisplayGroup();
        _displayGroup.setNumberOfObjectsPerBatch(batchSize);
        EODetailDataSource dds = new EODetailDataSource(
			masterObject.classDescription(),
			detailKey);
        _displayGroup.setDataSource(dds);
        _displayGroup.setMasterObject(masterObject);
        _displayGroup.setDetailKey(detailKey);
        _displayGroup.fetch();
    }
    return _displayGroup;
}

As before the WODisplayGroup API is here

Extending EOGenerator templates

The default EOGenerator templates (JavaSourceEOF52.eotemplate and JavaSubclassSourceEOF5.eotemplate) only implement the basic logic in the generated _EO.java and EO.java classes.

I’m assuming you know what eogenerator does, and you understand how the _EO.java and EO.java classes relate to one another. If you don’t, ask, and I’ll try to explain.

For instance, for an entity named Person with the attributes firstName and lastName and a to many relationship to PhoneNumbers named phoneNumbers you’d end up with this:

  • Getter and setter accessor methods for firstName
  • Getter and setter accessor methods for lastName
  • Getter and setter methods for phoneNumbers
  • addTo and removeFrom methods for phoneNumbers

This is about the same amount of java that you’d get from EOModeler if it generated your java for you. Acceptable, but we can do much better.

My Template

My templates (which you can download here) are derived from the advanced sample template that ships with eogenerator (EOGJavaSource.eotemplate) and create the following additional methods in your _EO.java class:

For each to-many relationships

  • addToRelationshipNameRelationship(Object obj) – calls addObjectToBothSidesOfRelationshipWithKey on the relationship with obj
  • removeFromRelationshipNameRelationship(Object obj) – calls removeObjectFromBothSidesOfRelationshipWithKey on the relationship with obj
  • createRelationshipNameRelationship() – creates a new object matching the destination type of the relationship, inserts it into this EO’s EOEditingContexts and adds it to the relationship.
  • deleteRelationshipNameRelationship(Object obj) – removes obj from the relationship and then calls editingContext().deleteObject(obj)
  • deleteAllRelationshipNameRelationships – iterates through all of the objects in the relationship, calling deleteRelationshipNameRelationship.

For each model defined fetch specifications

  • objectsForFetchSpecificationName(EOEditingContext ec, AObject aObjectBinding, BObject bObjectBinding, …) – a static method that returns any objects matching the fetch spec FetchSpecificationName. It takes typed objects for any bindings defined in the fetchSpec.

Using the templates

Using my templates is straight forward, simply specify them when you call EOGenerator (see this post for more details). Before you use my templates however, you will need to specify the name of the class that your EO’s should inherit from (EOGenericRecord is the default). Replace the reference to CBEOBaseClass in the constructor section of CBJavaSourceEOF5.eotemplate (it should be around line 29).

Extending the templates

EOGenerator uses a tool called MiscMerge to create the java class from the EOModel. There is a MiscMerge.rtf included in the eogenerator distribution, but you can figure out what is going on pretty easily just by looking at the templates (the template that does most of the work is CBJavaSourceEOF5.eotemplate).

One of the downsides to using Key Value Coding is that you lose compile time checking on valueForKey methods. For instance, calling:

valueForKey("anAtribute")

will compile fine, but fail at runtime if the attribute is actually called anAttribute.

So, one extension you may wish to make to my templates is to have static strings created to that map to each of your attributes. This means that you can call:

valueForKey(MyEO.AN_ATTRIBUTE)

and the compiler will catch your typos.

The EOGJavaSource.eotemplate included with eogenerator includes an example of how to do this. The template code between line 34 and 39 will create a static string variable for each of your attributes and relationships. Simply copy these lines of code (in between the <$comment … $> and <$comment) and paste them below the constructor in CBJavaSourceEOF5.eotemplate.

EOGenerator is a very powerful tool, it will get you a lot of machine generated (and more importantly machine maintained) code that will make your applications simpler and easier to build. I highly recommend exploring its capabilities a little.