Search code examples
javavariablesarraylistfreemarker

Override toString() for Freemarker Java List<Map> variable


I would like to create a Java Freemarker variable that works in a template both as

${myListOfMapsVar}     <#-- calls toString() on myListOfMapsVar -->

and

<#list myListOfMapsVar as item>
  ${item.mapKeyName}
</#list>

It seems like this should be possible using something like

List<Map> myListOfMaps = ...
var myListOfMapsVar = new MyWrappedObject();
myListOfMapsVar.wrap( myListOfMaps );
class MyWrappedObject extends DefaultObjectWrapper {
  MyWrappedObject() { super(); }

  public String toString() {
    return "a string representation of my list";
  }
}

However, when I try this, the ${myListOfMapsVar} works as expected, but the list enumeration produces an error:

The value you try to list is an extended_hash+string (.... wrapped into f.e.b.StringModel), thus you must specify two loop variables after the "as"; one for the key, and another for the value, like <#... as k, v>).

What am I doing wrong?

btw, I cannot change any application-wide Freemarker configuration.


Solution

  • For those that come later, here's my solution

    class MyListOfMap<K,V> implements TemplateSequenceModel, TemplateScalarModel {
    
        DefaultObjectWrapper wrapper = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_27).build();
        List<Map<K,V>> listOfMaps;
        Function<Map<K,V>,String> mapper;
    
        MyListOfMap( List list, Function<Map<K,V>,String> mapper ) {
          listOfMaps = list;
          this.mapper = mapper;
        }
    
        @Override
        public String getAsString() throws TemplateModelException {
          var strings = listOfMaps.stream().map( mapper ).collect(Collectors.toList());
          return String.join( ", ", strings );
        }
    
        @Override
        public TemplateModel get(int index) throws TemplateModelException {
          return new MapModel( listOfMaps.get(index), wrapper );
        }
    
        @Override
        public int size() throws TemplateModelException {
          return listOfMaps.size();
        }
      }
    
    var mylist = new ArrayList<Map<String,String>>();
    mylist.add( Map.of( "first", "John", "last", "Smith", "age", "32", "city", "Chicago", "State", "IL" ) );
    mylist.add( Map.of( "first", "Jane", "last", "Doe", "age", "29", "city", "Memphis", "State", "TN" ) );
    mylist.add( Map.of( "first", "Mark", "last", "Roy", "age", "38", "city", "Boston", "State", "MA" ) );
        
    var listOfPeople = new MyListOfMap<String,String>( mylist, 
            (person) -> person.get("first") + " " + person.get("last"));
        
    var dataModel.put( "people", listOfPeople );
    

    Variable 'people' can be used as a 'summary' string and as a list of hashes

    myftltemplate.ftl:

    ${people}  <#-- evaluates to: John Smith, Jane Doe, Mark Roy -->
    

    and can also be used like

    <#list people as person>
      <tr>
        <td>${person.first}</td>
        <td>${person.last}</td>
        <td>${person.age}</td>
        <td>${person.city}</td>
        <td>${person.state}</td>
      </tr>
    </#list>
    

    Thanks to @ddekany for the pointers.