We have a problem every year or so that someone on a Mac of PC will name a template with a different case than it is referenced in the code. For example, on disk it is /templates/mytemplate.ftl, and in the code it is loaded from "myTemplate.ftl". This works fine on the case-insensitive operating systems, but when we deploy to Linux it's sad panda time.
It would be great if the FileTemplateLoader could take an optional parameter that said whether to enforce case sensitivity. Most people would probably only want to turn this on during development since the only reliable way I could find to check file case sensitivity was to actually traverse the file hierarchy. Here is my code that extended FileTemplateLoader to achieve this:
import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.Reader; import java.util.Collections; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import com.cargurus.util.Constants; import freemarker.cache.FileTemplateLoader; /** * FileTemplateLoader that checks case of files. */ public class CaseSensitiveFileTemplateLoader extends FileTemplateLoader { private static final Set<String> VALIDATED_FILE_NAME = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); public CaseSensitiveFileTemplateLoader() throws IOException { } public CaseSensitiveFileTemplateLoader(File baseDir) throws IOException { super(baseDir); } public CaseSensitiveFileTemplateLoader(File baseDir, boolean allowLinking) throws IOException { super(baseDir, allowLinking); } @Override public Reader getReader(Object templateSource, String encoding) throws IOException { // Do first since it checks source type is File Reader reader = super.getReader(templateSource, encoding); if (!Constants.IS_PRODUCTION) { checkCaseSensitivity((File) templateSource); } return reader; } /** * Recursively check if case-sensitive file is found. */ private void checkCaseSensitivity(File targetFile) throws FileNotFoundException { if (VALIDATED_FILE_NAME.contains(targetFile.getPath())) { return; } File parent = targetFile.getParentFile(); if (parent != null) { for (File file : parent.listFiles()) { if (file.getName().equalsIgnoreCase(targetFile.getName())) { if (!file.getName().equals(targetFile.getName())) { throw new FileNotFoundException( "Template file name does not match case.\n" + "\nFile system: " + file.getName() + "\nUser: " + targetFile.getName()); } VALIDATED_FILE_NAME.add(targetFile.getPath()); checkCaseSensitivity(parent); break; } } } } }
I think this should be an option of
FileTemplateLoader
(no subclass is needed),checkFileNameCaseMatches
or like.Ideally, the default of that option should be
true
in development mode, except maybe if we can detect that we are on UN*X. We have the"freemarker.development"
system property for detecting developer mode, so the enclosing framework could set that if it's in developer mode.Yes, I was hoping it would be built into FileTemplateLoader, my code was just provided for the checkCaseSensitivity() example which hopefully you can leverage.
FYI, "if (parent != null)" needs to be "if ((parent != null) && (parent.listFiles() != null))" as we found out with a dev who uses symlinks.
You may want to test this:
The most significant difference from your implementation is that this works on the
findTemplateSource
level, which on one hand means that the system behaves much more like if it was running on Linux (or such), but on the other hand it can't provide an explanation in the error messages, it just says that the template wasn't found. (Though, in that error message the TemplateLoader's toString is also shown, which in that case will tell that emulateCaseSensitiveFileSystem was true, which can be a hint for the user.)Works great, thanks again for adding this!