WOLips XcodeIndex task

Prompted by my upgrade to Tiger, I dug in and got the XcodeIndex task from WOProject to work:

  • Add this target to your project’s build.xml file.
  • Make sure that the woproject.jar and cayenne.jar are in your class path. Either download WOProject and put the jars into /Developer/Java/Ant/lib/ or add the following to your CLASSPATH: /ECLIPSE_FOLDER/plugins/org.objectstyle.wolips.woproject_1.1.0.102/lib /ECLIPSE_FOLDER/plugins/org.objectstyle.wolips.cayenne_1.1.0.102/lib
  • From the command line, cd to your project’s folder and run: ant xcode.index
    It should create an Xcode.xcode bundle in your project.
  • Open the project in Xcode (ie: by clicking on Xcode.xcode/project.pbxproj). The Sources, Resources, and Frameworks groups should accurately represent your project.
  • Open a Component from the Resources group. WOBuilder should find all of the Classes and Attributes. Rejoice! You only need to do this for the first component after opening a project. Xcode needs to be running but once you’ve established the communication between Xcode and WOBuilder you can hide or minimize it and continue to work in Eclispe/WOLips as usual.
  • Re-run XcodeIndex whenever you add Components, Classes, or Frameworks to your project.

With XcodeIndex working, you will no longer need to apply the scary PrivateFrameworks hack for WOBuilder and EOModeler to operate correctly with Eclipse.

Tiger!

A couple of interesting tidbits pertaining to Tiger:

  • Apple has posted a API diff between WebObjects 5.2.3 and (a currently unreleased) WebObjects 5.2.4.
  • WebObjects 5.2.3 installs on Tiger and seems to run OK in both Xcode 2.0 and Eclipse/WOLips (granted I’ve done little testing beyond building and running my apps – if you see issues, post a comment).
  • For those with developer seed access, it looks like the GM build is now available from the ADC site.
  • Java 1.5 (coincidentally code named "Tiger") is available as a download from Apple as well. No word yet on WebObjects support.

Annoying WOBuilder bug

It is pretty well known that WOBuilder has its share of bugs. My current pet peeve is what it does to xhtml-compliant br tags. To see this for yourself:

  • Add a <br> tag to your component.
  • Switch to the source view and reformat the <br> so it is correctly formatted for xhtml (ie: <br />)
  • Switch back to layout view and select, cut and paste the <br />.
  • Switch to the source view and look at the result. It will look like this: <br /="">

Yuck!

Core Data

More and more details are becoming available about Core Data. I’ve mentioned it in the past, but with little detail due to the NDA nature of the information. However Apple has now published an extensive overview (thanks bbum).

I am hoping that this gets people really excited. I’m hoping that they start building cool data-driven Cocoa apps because they get this cool stuff for free. I’m hoping they go: “Gee this is neat! I wish we could build web-apps this way too.”

Because, then I could show them WebObjects. ‘Cause you know, you can.

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. 🙂