javasoy-templates

Migrating from SoyTofu to SoyTemplates for Java 17 upgrade


The app I work on currently uses Java 8. We're migrating to 17 and as part of that I need to convert some code which currently references SoyTofu to use SoySauce (Google Closure Templates). Here's the relevant parts of the class I'm working in. My goal is to have the lightest possible touch on this code, but I'm getting stymied at every turn. Note that I'm also a new Java developer, so take my lack of knowledge with a grain of salt.

import com.google.template.soy.SoyFileSet;
import com.google.template.soy.tofu.SoyTofu;

public class Foobar {
    ...
    private SoyTofu soyTemplates;
    ...
    @Override
    protected void startUp() throws Exception {
        ...
        String templatesProperty = "templates/mail.soy templates/mail-admin.soy";
        try {
            String[] templates = templatesProperty.split("\\s+");
            if (templates.length > 0) {
                SoyFileSet.Builder builder = SoyFileSet.builder();
                for (String t : templates) {
                    URL url = get.Url(t);
                    builder.add(url);
                }
                soyTemplates = builder.build().compileToTofu();
            }
        }
        ...
    }

    public Message compose(MailItem item) {
        ...
        Map<String, Object> ijData = new HashMap<String, Object>();
        ijData.put("supported", true);
        ijData.put("brand", "Accounts");

        String renderSubject = soyTemplates.newRenderer(item.subject).setData(item.data).setIjData(ijData).render();
        message.setSubject(renderSubject);

        String renderBody = soyTemplates.newRenderer(item.body).setData(item.data).setIjData(ijData).render();
        message.setContent(renderBody, MediaType.TEXT_HTML);
        ...
    }
}

And a snippet of one of the .soy files

/**
 * Account request submitted subject
 */
{template accountRequestSubmittedSubject}
{@param username : string}
{$ij.brand} account request: {$username}
{/template}

The GitHub repository for GCT has a migration doc, but it's different enough that I'm not able to get it working. It seems to indicate that I can compile the templates in real time, but doesn't suggest that it's a good idea; for speed purposes. I'm willing to take that hit though since there are only two templates.

Several parts of this code are showing deprecation warnings, which I can deal with after I get the migration working. The primary issue is that the injected data method doesn't appear to be working correctly.

When I run a unit test which references this code I get an exception

Caused by: com.google.template.soy.error.SoyCompilationException: errors during Soy compilation
file:/target/test-classes/templates/mail-admin.soy:8: error: Unknown variable.
8: {$ij.brand} account request: {$requestorUsername}
    ~~~

I've tried creating a separate SoySauce builder, I've tried converting the SoyTofu builder to SoySauce, but I keep hitting walls. Anyone have thoughts?


Solution

  • I finally got it to work. Here's how I did it.

    Template files need the following changes.

    1. Template blocks "names" no longer begin with a .
    Change from this:
    {template .accountRequestSubmittedSubject}
    to this
    {template accountRequestSubmittedSubject}
    
    1. Doc strings must be contained within template block instead of in comments above template block.
    Change from this:
    /**
    * @param requestorUsername : string
    */
    to this:
    {template accountRequestSubmittedSubject}
    {@param requestorUsername : string}
    
    1. The map param type must be expanded.
    Change from this:
    {@param requestFields : map}
    to this:
    {@param requestFields : map<string, string>} (or whatever the map being passed in consists of)
    
    1. The list param type must be expanded.
    Change from this:
    {@param groupsToJoin : list}
    to this
    {@param groupsToJoin : list<string>}
    
    1. The integer param type has been changed to int.
    Change from this:
    {@param totalProfilechange : integer}
    to this
    {@param totalProfilechange : int}
    
    1. The boolean param type has been changed to bool.
    Change from this:
    {@param cacEligible : boolean}
    to this
    {@param cacEligible : bool}
    
    1. The isFirst template method has been removed. One replacement approach might be to change from this:
    {if not isFirst($group)}, {/if}
    to this
    {if $groupsToJoin.indexOf($group) > 0}, {/if}
    
    1. The ifempty template method has been removed. One approach might be to replace with an explicit check against the array length:
    {if $groupsToLeave.length > 0}
    
    1. $ij variables have been replaced with their key names
    Change from this:
    {$ij.foo>}
    to this
    {$foo}
    
    1. Template blocks which use $ij must now have explicit callouts for the injected variable Add this, just inside the template block
    {@inject foo: string}
    

    Java rendering changes

    It's frustrating that the Closure Templates repo has deprecated code in their examples, which caused much headache. This is how I resolved it for our codebase. Hope this helps someone else out.

    import java.net.URL;
    import java.util.HashMap;
    import java.util.Map;
    import javax.ws.rs.core.MediaType;
    import com.google.template.soy.data.SanitizedContent;
    import com.google.template.soy.jbcsrc.api.SoySauce;
    import com.google.template.soy.SoyFileSet;
    
    public class MailService {
    
        private SoySauce soySauce;
    
        protected void startUp() {
            String template = "templates/foo.soy";
            SoyFileSet.Builder builder = SoyFileSet.builder();
            URL url = MailService.class.getClassLoader().getResource(template);
            builder.add(url, template);
            soySauce = builder.build().compileTemplates();
        }
    
        private Message compose(MailItem item) {
            Message message = messageSender.create();
    
            // Injected data (available to all templates)
            Map<String, Object> ijData = new HashMap<String, Object>();
            ijData.put("foo", "FOO");
    
            SoySauce.Renderer subjectRenderer = soySauce.renderTemplate(item.subject);
            String renderSubject = subjectRenderer.setData(item.data).setIj(ijData).renderText().get();
            message.setSubject(renderSubject);
    
            SoySauce.Renderer bodyRenderer = soySauce.renderTemplate(item.body);
            SanitizedContent renderBody = bodyRenderer.setData(item.data).setIj(ijData).renderHtml().get();
            message.setContent(renderBody.coerceToString(), MediaType.TEXT_HTML);
    
            return message;
        }
    }