Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use a single freemarker template to display tables of arbitrary pojos

Tags:

freemarker

Attention advanced Freemarker gurus:

I want to use a single freemarker template to be able to output tables of arbitrary pojos, with the columns to display defined separately than the data. The problem is that I can't figure out how to get a handle to a function on a pojo at runtime, and then have freemarker invoke that function (lambda style). From skimming the docs it seems that Freemarker supports functional programming, but I can't seem to forumulate the proper incantation.

I whipped up a simplistic concrete example. Let's say I have two lists: a list of people with a firstName and lastName, and a list of cars with a make and model. would like to output these two tables:

<table>
  <tr>
    <th>firstName</th>
    <th>lastName</th>
  </tr>
  <tr>
    <td>Joe</td>
    <td>Blow</d>
  </tr>
  <tr>
    <td>Mary</td>
    <td>Jane</d>
  </tr>
</table>

and

<table>
  <tr>
    <th>make</th>
    <th>model</th>
  </tr>
  <tr>
    <td>Toyota</td>
    <td>Tundra</d>
  </tr>
  <tr>
    <td>Honda</td>
    <td>Odyssey</d>
  </tr>
</table>

But I want to use the same template, since this is part of a framework that has to deal with dozens of different pojo types.

Given the following code:

public class FreemarkerTest {

  public static class Table {
    private final List<Column> columns = new ArrayList<Column>();

    public Table(Column[] columns) {
      this.columns.addAll(Arrays.asList(columns));
    }

    public List<Column> getColumns() {
      return columns;
    }

  }

  public static class Column {
    private final String name;

    public Column(String name) {
      this.name = name;
    }

    public String getName() {
      return name;
    }
  }

  public static class Person {
    private final String firstName;
    private final String lastName;

    public Person(String firstName, String lastName) {
      this.firstName = firstName;
      this.lastName = lastName;
    }

    public String getFirstName() {
      return firstName;
    }

    public String getLastName() {
      return lastName;
    }
  }

  public static class Car {
    String make;
    String model;

    public Car(String make, String model) {
      this.make = make;
      this.model = model;
    }

    public String getMake() {
      return make;
    }

    public String getModel() {
      return model;
    }
  }

  public static void main(String[] args) throws Exception {
    final Table personTableDefinition = new Table(new Column[] { new Column("firstName"), new Column("lastName") });
    final List<Person> people = Arrays.asList(new Person[] { new Person("Joe", "Blow"), new Person("Mary", "Jane") });
    final Table carTable = new Table(new Column[] { new Column("make"), new Column("model") });
    final List<Car> cars = Arrays.asList(new Car[] { new Car("Toyota", "Tundra"), new Car("Honda", "Odyssey") });

    final Configuration cfg = new Configuration();
    cfg.setClassForTemplateLoading(FreemarkerTest.class, "");
    cfg.setObjectWrapper(new DefaultObjectWrapper());
    final Template template = cfg.getTemplate("test.ftl");

    process(template, personTableDefinition, people);
    process(template, carTable, cars);
  }

  private static void process(Template template, Table tableDefinition, List<? extends Object> data) throws Exception {
    final Map<String, Object> dataMap = new HashMap<String, Object>();
    dataMap.put("tableDefinition", tableDefinition);
    dataMap.put("data", data);
    final Writer out = new OutputStreamWriter(System.out);
    template.process(dataMap, out);
    out.flush();
  }

}

All the above is a given for this problem. So here is the template I have been hacking on. Note the comment where I am having trouble.

<table>
  <tr>
<#list tableDefinition.columns as col>
    <th>${col.name}</th>
</#list>
  </tr>
<#list data as pojo>
  <tr>
<#list tableDefinition.columns as col>
    <td><#-- what goes here? --></td>    
</#list>  
  </tr>
</#list>
</table>

So col.name has the name of the property I want to access from the pojo. I have tried a few things, such as

pojo.col.name

and

<#assign property = col.name/>
${pojo.property}

but of course these don't work, I just included them to help convey my intent. I am looking for a way to get a handle to a function and have freemarker invoke it, or perhaps some kind of "evaluate" feature that can take an arbitrary expression as a string and evaluate it at runtime.

like image 950
Kevin Pauli Avatar asked Apr 28 '10 22:04

Kevin Pauli


2 Answers

?eval is (almost?) always a bad idea, because it often comes with performance drawbacks (e.g. a lot of parsing) and security problems (e.g. "FTL injection").

A better approach is using the square bracket syntax:

There is an alternative syntax if we want to specify the subvariable name with an expression: book["title"]. In the square brackets you can give any expression as long as it evaluates to a string.

(From the FreeMarker documentation about retrieving data from a hash)

In your case I'd recommend something like ${pojo[col.name]}.

like image 124
Chaquotay Avatar answered Oct 15 '22 06:10

Chaquotay


Found the answer.

${("pojo." + col.name)?eval}
like image 38
Kevin Pauli Avatar answered Oct 15 '22 05:10

Kevin Pauli