spring-bootfreemarkercdn

Freemarker Fallback Not Working with Multiple Template Loaders in Spring Boot


I'm working on a Spring Boot project where I use Freemarker for templating. I have configured multiple TemplateLoaders in Freemarker's configuration to load templates from two different CDN URLs: a baselineUrl and a dynamicUrl. The idea is that if a template is not found in the baselineUrl, it should fall back to the dynamicUrl.

Here is my configuration :

@Slf4j
@Configuration
@ConfigurationProperties("email")
@Data
public class FtlConfiguration {
  // https://dynamic/bhnpay-static/notification-template/ftl
  private String baselineUrl;
  // https://baseline/bhnpay-static/notification-template/ftl
  private String dynamicUrl;
  private long retentionTimeMillis;

  @Bean("emailConfiguration")
  public freemarker.template.Configuration createFtlConfigBean() throws IOException {
    freemarker.template.Configuration cfg = new freemarker.template.Configuration(
        freemarker.template.Configuration.VERSION_2_3_31);
    cfg.setDefaultEncoding(NotificationConstants.UTF_8);
    cfg.setLogTemplateExceptions(false);
    cfg.setWrapUncheckedExceptions(true);
    cfg.setFallbackOnNullLoopVariable(false);
    cfg.setLocalizedLookup(false);
    List<TemplateLoader> loaders = new ArrayList<>();
    // CloudTemplateLoader is just a wrapper on UrlTemplateLoader
    loaders.add(new CloudTemplateLoader(new URL(baselineUrl)));
    loaders.add(new CloudTemplateLoader(new URL(dynamicUrl)));
    cfg.setTemplateLoader(
        new MultiTemplateLoader(loaders.toArray(new TemplateLoader[0])));
    cfg.setCacheStorage(new MruCacheStorage(5000, Integer.MAX_VALUE));
    cfg.setTemplateUpdateDelayMilliseconds(retentionTimeMillis);
    return cfg;
  }

}

Below is how i am trying to load template :

@Component
@Slf4j
public class FtlTemplateHandler {

    private final Configuration configuration;

    @Autowired
    public FtlTemplateHandler(@Qualifier("emailConfiguration") Configuration configuration) {
        this.configuration = configuration;
    }

    public String getTemplateAsHtml(String emailTemplateUrl) {
        try {
            Template template = configuration.getTemplate(emailTemplateUrl);
            // Process the template
        } catch (IOException e) {
           try {
             log.info("removing template from cache {}", emailTemplateUrl);
             configuration.removeTemplateFromCache(emailTemplateUrl);
             Thread.sleep(10000);
           } catch (IOException | InterruptedException ex) {
             // process exception
           }
       }
    }
}

My cloud template loader :

@Slf4j
public class CloudTemplateLoader extends URLTemplateLoader {
  private URL root;
  public CloudTemplateLoader(URL root) {
    super();
    this.root = root;
  }
  public URL getRoot() {
    return root;
  }
  public void setRoot(URL root) {
    this.root = root;
  }
  @Override
  protected URL getURL(String template) {
    try {
      return new URL(root, template);
    } catch (MalformedURLException e) {
      log.error("Error while loading templates from cloud.");
    }
    return null;
  }
}

Getting below error :

Caused by: java.io.IOException: Server returned HTTP response code: 403 for URL: https://baseline/bhnpay-static/notification-template/ftl/en-US/b9b09aa4-fd39-4812-924e-1fe7f6d4ed51.ftl
    at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:2000)
    at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1589)
    at java.base/sun.net.www.protocol.http.HttpURLConnection.getHeaderField(HttpURLConnection.java:3256)
    at java.base/java.net.HttpURLConnection.getHeaderFieldDate(HttpURLConnection.java:601)
    at java.base/java.net.URLConnection.getLastModified(URLConnection.java:567)
    at java.base/sun.net.www.protocol.https.HttpsURLConnectionImpl.getLastModified(HttpsURLConnectionImpl.java:392)
    at freemarker.cache.URLTemplateSource.lastModified(URLTemplateSource.java:94)
    at freemarker.cache.URLTemplateLoader.getLastModified(URLTemplateLoader.java:50)
    at freemarker.cache.MultiTemplateLoader$MultiSource.getLastModified(MultiTemplateLoader.java:142)
    at freemarker.cache.MultiTemplateLoader.getLastModified(MultiTemplateLoader.java:99)
    at freemarker.cache.TemplateCache.getTemplateInternal(TemplateCache.java:439)

but the template is present at the dynamic url, that means after looking at baseline cdn it is not checking the dynamic cdn.

I have verified that the URLs are correct and accessible. I tried clearing the cache to ensure it's not a caching issue, but the fallback still doesn't work.


Solution

  • Validating the connection to the url before returning it worked for me actually.

    @Override
      protected URL getURL(String template) {
        try {
          URL url = new URL(root, template);
    
          if (isTemplateAvailable(url)) {
            return url;
          } else {
            log.warn("Template not found or inaccessible at URL: {}", url);
          }
        } catch (MalformedURLException e) {
          log.error("Malformed URL when loading template {}: ", template, e);
        } catch (IOException e) {
          log.error("IOException when checking template availability: ", e);
        }
    
        return null;
      }
    
      private boolean isTemplateAvailable(URL url) throws IOException {
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("HEAD");
        connection.connect();
        int responseCode = connection.getResponseCode();
        connection.disconnect();
        return responseCode == HttpURLConnection.HTTP_OK;
      }