Wednesday, March 11, 2009

Generating Strongly-Typed Properties Classes With openArchitectureWare, Part 4: Generating Java Classes

In the fourth part of this series, I'm finally read to generate strongly-typed wrappers for Java .properties files. If you would like to review how I got to this point, check out the earlier installments here, here, and here.

First, a little bit about my code generation philosophy. I prefer to only generate specializations -- the things that will actually vary from model to model -- when possible. Inheritance works well for this: I can put common functionality in base classes and generate subclasses. This cuts down on the amount of code that I have to work with in the template. As an added bonus, the base class can be tested (and perhaps even used) independently of the code generation process.

With that in mind, I will start by creating base classes. Recall that my PropertySet is defined as supporting two different "load types" -- resource bundle and classloader resource. That means two base classes, which I will call ResourceBundleProperties and ClassLoaderResourceProperties. Since my goal is to demonstrate openArchitectureWare, model-driven development, and code generation, and not to write the be-all-and-end-all of .properties file loaders, this code won't support everything you can imagine. Generally speaking, I'm following the tried-and-true approaches described here.

ResourceBundleProperties.java

package com.scottmcmaster365.properties;

import java.util.Properties;

/**
 * Load a properties file from a resource bundle
 * following the approach described here:
 *  http://www.javaworld.com/javaworld/javaqa/2003-08/01-qa-0808-property.html?page=2
 * @author scottmcm
 *
 */
public class ResourceBundleProperties
{
 protected java.util.Properties properties;
 
 /**
  * Load the properties from the resource bundle with the given name.
  * @param name
  */
 public ResourceBundleProperties(String name)
 {
  ClassLoader loader = Thread.currentThread ().getContextClassLoader();

  final java.util.ResourceBundle rb = java.util.ResourceBundle
    .getBundle(name, java.util.Locale.getDefault(), loader);

  properties = new java.util.Properties();
  for (java.util.Enumeration<String> keys = rb.getKeys(); keys
    .hasMoreElements();) {
   final String key = (String) keys.nextElement();
   final String value = rb.getString(key);

   properties.put(key, value);
  } 
  
  if (properties == null) {
   throw new IllegalArgumentException("Could not load " + name + " as resource bundle");
  }
 }
 
 public Properties getProperties() {
  return properties;
 }
}


ClassLoaderResourceProperties.java

package com.scottmcmaster365.properties;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * Load a properties file from a resource bundle
 * following the approach described here:
 *  http://www.javaworld.com/javaworld/javaqa/2003-08/01-qa-0808-property.html?page=2
 * @author scottmcm
 *
 */
public class ClassLoaderResourceProperties
{
 protected java.util.Properties properties;
 
 private static final String SUFFIX = ".properties";

 /**
  * Load the properties from the resource bundle with the given name.
  * @param name
  * @throws IOException 
  */
 public ClassLoaderResourceProperties(String name) throws IOException
 {
  ClassLoader loader = Thread.currentThread ().getContextClassLoader();

  InputStream in = null;
  try {
   if (!name.endsWith(SUFFIX)) {
    name = name.concat(SUFFIX);
   }

   in = loader.getResourceAsStream(name);
   if (in != null) {
    properties = new java.util.Properties();
    properties.load(in);
   }
  } finally {
   if (in != null)
    try {
     in.close();
    } catch (Throwable ignore) {
    }
  }
  
  if (properties == null) {
   throw new IllegalArgumentException("Could not load " + name + " as classloader resource");
  }
 }
 
 public Properties getProperties() {
  return properties;
 }
}


Next I create a code generation template (an .xpt file, written in the oAW Xpand template language) to create a subclass based on the information in PropertySet models.

JavaWrapperTemplate.xpt

«IMPORT metamodel»

«DEFINE javaClass FOR properties::PropertySet»
 «FILE Package.replaceAll("\\.", "/") + "/" + Name + ".java"»
  package «Package»;
  
  /**
   * Load a properties file based on the approach described here:
   *   http://www.javaworld.com/javaworld/javaqa/2003-08/01-qa-0808-property.html?page=2
   */
  public class «Name-»
  «IF LoadAs == properties::LoadType::CLASSLOADER_RESOURCE-»
   extends com.scottmcmaster365.properties.ClassLoaderResourceProperties
  «ELSE-»
   extends com.scottmcmaster365.properties.ResourceBundleProperties
  «ENDIF-»
  {
   public «Name»() 
    «IF LoadAs == properties::LoadType::CLASSLOADER_RESOURCE»
     throws java.io.IOException
    «ENDIF»
   {
    super("«ResourceName»");
   }
   
   «EXPAND property FOREACH Properties»
   «EXPAND propertyWithDefault FOREACH Properties»
  }
 «ENDFILE»
«ENDDEFINE»

«DEFINE property FOR properties::Property»
 public «Type» get«Name»() {
  «IF Type == properties::PropertyType::Integer»
   return Integer.parseInt(properties.getProperty("«Name»"));
  «ELSE»
   return properties.getProperty("«Name»");
  «ENDIF»
 }
«ENDDEFINE»

«DEFINE propertyWithDefault FOR properties::Property»
 public «Type» get«Name»(String defaultValue) {
  «IF Type == properties::PropertyType::Integer»
   return Integer.parseInt(properties.getProperty("«Name»", defaultValue));
  «ELSE»
   return properties.getProperty("«Name»", defaultValue);
  «ENDIF»
 }
«ENDDEFINE»

While not quite as simple as the template I presented last time to generate sample .properties file, this is still pretty straightforward. Notice how I determine which base class to extend using the value of LoadAs defined in the input model. The last two Xpand templates for Property generate overloaded getProperty() methods, with and without the default value.

Before I can use this template, I need to wire it into my oAW workflow. So I make the following addition to generator.oaw, right after checking the model with the .chk file:
<!--  generate code -->
<component class="org.openarchitectureware.xpand2.Generator">
 <metaModel idRef="mm"/>
 <expand
  value="template::JavaWrapperTemplate::javaClass FOR model" />
 <outlet path="${src-gen}" >
  <postprocessor class="org.openarchitectureware.xpand2.output.JavaBeautifier" />
 </outlet>
</component>


Now when I invoke generator.oaw as an oAW workflow using my sample PropertySet model, I get the following code generated into my project's src-gen directory:

MyProperties.java

package com.scottmcmaster365.propertiessample;

/**
 * Load a properties file based on the approach described here:
 *   http://www.javaworld.com/javaworld/javaqa/2003-08/01-qa-0808-property.html?page=2
 */
public class MyProperties
  extends
   com.scottmcmaster365.properties.ClassLoaderResourceProperties {
 public MyProperties()

 throws java.io.IOException

 {
  super("MyProperties.properties");
 }

 public Integer getMyIntegerProperty() {

  return Integer.parseInt(properties.getProperty("MyIntegerProperty"));

 }

 public String getMyStringProperty() {

  return properties.getProperty("MyStringProperty");

 }

 public Integer getMyIntegerProperty(String defaultValue) {

  return Integer.parseInt(properties.getProperty("MyIntegerProperty",
    defaultValue));

 }

 public String getMyStringProperty(String defaultValue) {

  return properties.getProperty("MyStringProperty", defaultValue);

 }

}

The benefit of having a strongly-typed wrapper can be seen in this unit test example:
@Test
public void testLoadMyProperties() throws IOException
{
 MyProperties properties = new MyProperties();
 assertEquals(2, properties.getMyIntegerProperty());
 assertEquals("hello world", properties.getMyStringProperty());
}


And that's just about it. If I want to use my new properties wrapper in a real project, I will need to pull in all of the files from this project and add some sort of "generate" target to my Ant build which uses the oAW Ant task to invoke generator.oaw.

I should point out that there is a notable overhead involved in spinning up the oAW code generation pipeline, and if you have to do it multiple times per build, your build process can slow down considerably. It is therefore beneficial to consolidate into as few model files (.xmi's) as possible. So a possible enhancement to what I have done here would be to metamodel a class which is a set of PropertySets and then enhance my Xpand templates to generate multiple files, so that I can cram more than one PropertySet into a model.

I hope that after reading this series, you're ready to run out and EMF, oAW, modeling, and code generation. If you do, please share your experiences with me, because I'm always looking out for new techniques and best practices.

No comments: