Notes on Apache Velocity

Russell Bateman
April 2016
last update:

Apache Velocity Javadoc is here.

Terminology

(To be corrected as needed.)


Apache Velocity and resources

When you open a template, it's going to be typically in the filesystem, but where? In the example below, I want to open a template named, main.vm, but in turn, it wants to include another template that gets processed.

However, in order to find either template, which I keep on path src/main/resources, I've got to tell Velocity about this.

main.vm:
#** This is the main template; it includes a subtemplate. *#
#parse( "telecom.vm" )
telecom.vm:
#if( $phone_number )
<telecom>
  <system code="phone" />
  #if( !$phone_extension )
  <value value="$phone_areacode $phone_number" />
  #else
  <value value="$phone_areacode $phone_number, x$phone_extension" />
  #end
  #if( $phone_type )
  <use code="$phone_type" />
  #end
</telecom>
#end
DemonstrateInclude.java:
package com.etretatlogiciels;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;

import java.io.StringWriter;
import java.util.Properties;

public class DemonstrateInclude
{
  public static void main( String[] args ) throws Exception
  {
    VelocityEngine engine = new VelocityEngine();
    Properties properties = new Properties();
    properties.setProperty( "file.resource.loader.path", "src/main/resources" );
    engine.init( properties );

    VelocityContext context = new VelocityContext();
    context.put( "phone_extension", "9" );
    context.put( "phone_areacode",  "801" );
    context.put( "phone_number",    "295-9626" );
    context.put( "phone_type",      "home" );

    Template template = engine.getTemplate( "main.vm" );

    StringWriter writer = new StringWriter();

    template.merge( context, writer );

    System.out.println( writer.toString() );
  }
}

Using a string as template

In their stupendous foresight, Velocity developers knew no one would ever want to pass a template to the engine as a string—only as a resource.

Here's how to hack that.

package com.etretatlogiciels;

import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.resource.loader.StringResourceLoader;
import org.apache.velocity.runtime.resource.util.StringResourceRepository;

/**
 * Rig Apache Velocity to accept only a string (and not a .vm file
 * as a template. It rolls the string into a resource that it makes known
 * to Velocity under a special name such that later the call to
 * getTemplate( name ) will get the contents of the string
 * as if out of a resource file.
 * @author Russell Bateman
 * @since April 2016
 */
public class VelocityRigger
{
  /**
   * Pass the template as a string and also the name by which it should be recognized.
   * A Velocity engine will be created, rigged up with the template and then passed
   * back for use later when calling engine.getTemplate( String templateName ).
   * @param templateName that will be passed to engine.getTemplate( String )
   * @param templateString to use as the contents of the resource.
   * @return a rigged Velocity engine.
   */
  public static VelocityEngine getEngineReadyWithTemplateString( String templateName, String templateString )
  {
    VelocityEngine engine = new VelocityEngine();
    // engine.setProperty( RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, "org.apache.velocity.runtime.log.Log4JLogChute" );
    engine.setProperty( Velocity.RESOURCE_LOADER, "string" );
    engine.addProperty( "string.resource.loader.class", StringResourceLoader.class.getName() );
    engine.addProperty( "string.resource.loader.repository.static", "false" );
    // engine.addProperty("string.resource.loader.modificationCheckInterval", "1");
    engine.init();

    StringResourceRepository repo = ( StringResourceRepository ) engine.getApplicationAttribute( StringResourceLoader.REPOSITORY_NAME_DEFAULT );
    repo.putStringResource( templateName, templateString );

    return engine;
  }
}

Calling it: you see the old code commented out and the new code called.

    // initialize engine...
//    VelocityEngine ve = new VelocityEngine();
//    ve.init();
    VelocityEngine ve = VelocityRigger.getEngineReadyWithTemplateString( "mud-index.vm", TEMPLATE_CONTENTS );

    // get the template...
//    Template template = ve.getTemplate( new StandardResource( "mud-index.vm" ).resourcePath() );
    Template template = ve.getTemplate( "mud-index.vm" );

Using VTL macros as variables with hyphens, dots, etc.

Rewriting with hyphens, dots, etc. can be problematic. In the sample below, assuming $filename, Velocity will blow chunks (MethodInvocationException) saying "Variable, property or method unset or missing in the source". This is because it mistakes $filename-metadata.xml for a VTL macro name:

<?xml version="1.0" encoding="UTF-8"?>
<advance-directives>
  <filenames>
    <filename type="advance-directives" />$filename</filename>
    <filename type="metadata" />$filename-metatdata.xml</filename>
  </filenames>
</advance-directives>

Instead, write it with braces:

<?xml version="1.0" encoding="UTF-8"?>
<advance-directives>
  <filenames>
    <filename type="advance-directives" />$filename</filename>
    <filename type="metadata" />${filename}-metatdata.xml</filename>
  </filenames>
</advance-directives>

Checking for variable definitions and leaving out content

It's possible to check for the existence/definition of a variable such that proposed content including variable evaluation is left out from a template:

<name>
  #if( $lastname )
  <family value="$lastname" />
  #end
  #if( $firstname )
  <given value="$firstname" />
  #end
  #if( $middlename )
  <given value="$middlename" />
  #end
</name>

So, from the above, <name>...</name> will always be written. However, Velocity supports logical && and || such that we can guard against emitting the <name> paragraph:

#if( $lastname || $firstname || $middlename )
  <name>
    #if( $lastname )
    <family value="$lastname" />
    #end
    #if( $firstname )
    <given value="$firstname" />
    #end
    #if( $middlename )
    <given value="$middlename" />
    #end
  </name>
#end

Doing hairy stuff: telephone numbers

Here's an exercise I don't feel entirely debonnaire about which is to say that, to apply it to a different case of what I might wish to do, I have to adapt this code carefully to the new case, changing one little thing at a time. I can't just jot all this code out blindly (in VTL, though the Java is no problem for me). I just still have a rough time lining up VTL with underlying Java in my tiny brain.

This case is a list of phone numbers, actually, three lists including:

Velocity provides no way, as far as I know, to loop through three separate arrays/lists at a time since it provides only the foreach construct. Therefore, the three lists much be unified under one. This is how I was able to do it.

  1. I create the unifying map: there will be an ordering (integer) key that will grow in magnitude as I fill the map (I don't show this, I only use LinkedHashMap to ensure order). This is because I arbitrarily want an order that doesn't necessarily need to exist (probably said too much about this).
  2. Next, I create a list of the three fields whose order is customary: area code, telephone number, optional extension. Each list is added to the unifying map in successive numeric keys (as discussed).
  3. Last, I punch the unifying map into Velocity context.
  4. In the template file...
  5. I bracket the (FHIR) XML I wish to generate with foreach to iterate the unifying map.
  6. Next, I use the assignment construct (#set()) to establish two very useful things:
    1. a shortcut called phoneList to reduce the verbosity of references I'd have to scatter all over in the template,
    2. a macro holding whether or not there is an extension—easier to check, again, than some long, yucky reference.
  7. Last, I create the two rewrites that will be replaced with the lovingly formatted telephone number: one for when it has an extension and another, the default, for when it does not have one.
TelecomHashMapWithList.java:
package com.etretatlogiciels;

import java.io.StringWriter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;

/**
 * Demonstrates how to reference a map from a template file.
 */
public class TelecomHashMapWithList2
{
  public static void main( String[] args )
  {
    Map< String, List< String > > phoneMap = new LinkedHashMap<>();

    List< String > phoneNumber;      // a list of area code, number and optional extension...

    phoneNumber = new ArrayList<>();
    phoneNumber.add( "801" ); phoneNumber.add( "295-9626" );
    phoneMap.put( "1", phoneNumber );

    phoneNumber = new ArrayList<>();
    phoneNumber.add( "510" ); phoneNumber.add( "555-1212" ); phoneNumber.add( "2345" );
    phoneMap.put( "2", phoneNumber );

    phoneNumber = new ArrayList<>();
    phoneNumber.add( "385" ); phoneNumber.add( "375-8733" );
    phoneMap.put( "3", phoneNumber );

    VelocityEngine  engine  = new VelocityEngine();
    VelocityContext context = new VelocityContext();
    context.put( "phoneMap", phoneMap );

    Template template = engine.getTemplate( new StandardResource( "telecom.vm" ).resourcePath() );
    StringWriter writer   = new StringWriter();

    template.merge( context, writer );
    System.out.println( writer );
  }
}
telecom.vm:
#foreach( $number in $phoneMap.keySet() )
  #set( $phoneList = $phoneMap.get( $number ) )
  #set( $hasExtension = ( $phoneList.size() > 2 ) )

  <telecom>

    #if( $hasExtension )

      <value value="($phoneList.get( 0 )) $phoneList.get( 1 ), x$phoneList.get( 2 )" />

    #else

      <value value="($phoneList.get( 0 )) $phoneList.get( 1 )" />

    #end

  </telecom>
#end

Output (cleaned up with an XML pretty printer an highlighting added to the bits replaced by the macro):

<telecom>
  <value value="(801) 295-9626" />
</telecom>
<telecom>
  <value value="(510) 555-1212, x2345" />
</telecom>
<telecom>
  <value value="(385) 375-8733" />
</telecom>

Note: yes, there's more to FHIR telecom than this, but that's not the purpose of this note.