Menu

#118 Make FileTemplateLoader conditionally case-sensitive

2.3.23
closed-fixed
nobody
None
5
2015-09-10
2015-03-27
No

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;
                }        
            }
        }
    }
}

Discussion

  • Dániel Dékány

    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.

     
  • Dániel Dékány

    • status: open --> open-accepted
     
  • Dániel Dékány

    • Group: undecided --> 2.3.23
     
  • Jasper Rosenberg

    Yes, I was hoping it would be built into FileTemplateLoader, my code was just provided for the checkCaseSensitivity() example which hopefully you can leverage.

     
  • Jasper Rosenberg

    FYI, "if (parent != null)" needs to be "if ((parent != null) && (parent.listFiles() != null))" as we found out with a dev who uses symlinks.

     
  • Dániel Dékány

    You may want to test this:

    Added FileTemplateLoader.setEmulateCaseSensitiveFileSystem(boolean). This is handy when you are developing on Windows but will deploy to a platform with case sensitive file system. The default is false, and true is only meant for development, not for production installations. The default can be overridden by setting the org.freemarker.emulateCaseSensitiveFileSystem system property to true.

    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.)

     
  • Dániel Dékány

    • status: open-accepted --> closed-fixed
     
  • Jasper Rosenberg

    Works great, thanks again for adding this!

     

Log in to post a comment.