FileAndPathExtension.java

package org.freshmarker.file;

import org.freshmarker.api.BuiltIn;
import org.freshmarker.api.extension.BuiltInProvider;
import org.freshmarker.api.extension.Register;
import org.freshmarker.api.extension.TemplateObjectProviders;
import org.freshmarker.api.extension.support.BuiltInRegister;
import org.freshmarker.core.ProcessContext;
import org.freshmarker.core.ProcessException;
import org.freshmarker.core.model.TemplateObject;
import org.freshmarker.core.model.primitive.TemplateString;
import org.freshmarker.api.TemplateObjectProvider;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.List;
import java.util.function.Function;

public class FileAndPathExtension implements BuiltInProvider, TemplateObjectProviders {

    private static final String SHA_1 = "SHA-1";

    private TemplateObject processFile(TemplateObject value, Function<File, ?> function, ProcessContext context) {
        File input = ((TemplateFile) value).getValue();
        return context.mapObject(function.apply(input));
    }

    private TemplateObject processPath(TemplateObject value, Function<Path, ?> function, ProcessContext context) {
        Path input = ((TemplatePath) value).getValue();
        return context.mapObject(PathOperations.execute(input, "inspect", function::apply));
    }

    private TemplateObject pathOperation(TemplateObject value, String name,
                                        PathOperations.Operation<?> operation, ProcessContext context) {
        Path path = value instanceof TemplatePath input ? input.getValue() : ((TemplateFile) value).getValue().toPath();
        return context.mapObject(PathOperations.execute(path, name, operation));
    }

    @Override
    public Register<Class<? extends TemplateObject>, String, BuiltIn> provideBuiltInRegister() {
        BuiltInRegister register = new BuiltInRegister();
        register.add(TemplateFile.class, "exists", (x, y, e) -> processFile(x, File::exists, e));
        register.add(TemplateFile.class, "is_directory", (x, y, e) -> processFile(x, File::isDirectory, e));
        register.add(TemplateFile.class, "is_file", (x, y, e) -> processFile(x, File::isFile, e));
        register.add(TemplateFile.class, "can_execute", (x, y, e) -> processFile(x, File::canExecute, e));
        register.add(TemplateFile.class, "can_read", (x, y, e) -> processFile(x, File::canRead, e));
        register.add(TemplateFile.class, "can_write", (x, y, e) -> processFile(x, File::canWrite, e));
        register.add(TemplateFile.class, "size", (x, y, e) -> processFile(x, File::length, e));
        register.add(TemplateFile.class, "length", (x, y, e) -> processFile(x, PathOperations::length, e));
        register.add(TemplateFile.class, "name", (x, y, e) -> processFile(x, File::getName, e));
        register.add(TemplateFile.class, "parent", (x, y, e) -> processFile(x, File::getParentFile, e));
        register.add(TemplatePath.class, "exists", (x, y, e) -> processPath(x, Files::exists, e));
        register.add(TemplatePath.class, "is_directory", (x, y, e) -> processPath(x, Files::isDirectory, e));
        register.add(TemplatePath.class, "is_file", (x, y, e) -> processPath(x, Files::isRegularFile, e));
        register.add(TemplatePath.class, "can_execute", (x, y, e) -> processPath(x, Files::isExecutable, e));
        register.add(TemplatePath.class, "can_read", (x, y, e) -> processPath(x, Files::isReadable, e));
        register.add(TemplatePath.class, "can_write", (x, y, e) -> processPath(x, Files::isWritable, e));
        register.add(TemplatePath.class, "size", (x, y, e) -> processPath(x, PathOperations::size, e));
        register.add(TemplatePath.class, "length", (x, y, e) -> processPath(x, PathOperations::length, e));
        register.add(TemplatePath.class, "name", (x, y, e) -> processPath(x, PathOperations::name, e));
        register.add(TemplatePath.class, "parent", (x, y, e) -> processPath(x, Path::getParent, e));
        register.add(TemplatePath.class, "md5", (x, y, e) -> checksum((TemplatePath)x, "MD5"));
        register.add(TemplatePath.class, "sha", (x, y, e) -> checksum((TemplatePath)x, SHA_1));
        register.add(TemplatePath.class, "sha1", (x, y, e) -> checksum((TemplatePath)x, SHA_1));
        register.add(TemplatePath.class, "sha256", (x, y, e) -> checksum((TemplatePath)x, "SHA-256"));
        register.add(TemplatePath.class, "sha384", (x, y, e) -> checksum((TemplatePath)x, "SHA-384"));
        register.add(TemplatePath.class, "sha512", (x, y, e) -> checksum((TemplatePath)x, "SHA-512"));
        register.add(TemplateFile.class, "md5", (x, y, e) -> checksum((TemplateFile)x, "MD5"));
        register.add(TemplateFile.class, "sha", (x, y, e) -> checksum((TemplateFile)x, SHA_1));
        register.add(TemplateFile.class, "sha1", (x, y, e) -> checksum((TemplateFile)x, SHA_1));
        register.add(TemplateFile.class, "sha256", (x, y, e) -> checksum((TemplateFile)x, "SHA-256"));
        register.add(TemplateFile.class, "sha384", (x, y, e) -> checksum((TemplateFile)x, "SHA-384"));
        register.add(TemplateFile.class, "sha512", (x, y, e) -> checksum((TemplateFile)x, "SHA-512"));
        for (Class<? extends TemplateObject> type : List.of(TemplatePath.class, TemplateFile.class)) {
            register.add(type, "is_symbolic_link", (x, y, e) -> pathOperation(x, "inspect link", Files::isSymbolicLink, e));
            register.add(type, "exists_no_follow", (x, y, e) -> pathOperation(x, "check existence of",
                    path -> Files.exists(path, LinkOption.NOFOLLOW_LINKS), e));
            register.add(type, "link_target", (x, y, e) -> pathOperation(x, "read link target of", Files::readSymbolicLink, e));
            register.add(type, "real_path", (x, y, e) -> pathOperation(x, "resolve", Path::toRealPath, e));
            register.add(type, "children", (x, y, e) -> pathOperation(x, "list children of", PathOperations::children, e));
        }
        return register;
    }

    private TemplateObject checksum(TemplatePath x, String algorithm) {
        return checksum(algorithm, x.getValue());
    }

    private TemplateObject checksum(TemplateFile x, String algorithm) {
        return  checksum(algorithm, x.getValue().toPath());
    }

    private static TemplateString checksum(String algorithm, Path value) {
        return PathOperations.execute(value, "calculate " + algorithm + " digest of", path -> checksumContent(algorithm, path));
    }

    private static TemplateString checksumContent(String algorithm, Path value) throws IOException {
        try (SeekableByteChannel ch = Files.newByteChannel(value, StandardOpenOption.READ)) {
            MessageDigest md = MessageDigest.getInstance(algorithm);
            ByteBuffer bf = ByteBuffer.allocate(1000);
            while (ch.read(bf) != -1) {
                bf.flip();
                md.update(bf);
                bf.clear();
            }
            byte[] digest = md.digest();
            return new TemplateString(HexFormat.of().formatHex(digest));
        } catch (NoSuchAlgorithmException ex) {
            throw new ProcessException("unsupported digest algorithm: " + algorithm, ex);
        }
    }

    @Override
    public List<TemplateObjectProvider> provideProviders() {
        return List.of(
                (environment, object) -> object instanceof Path path ? new TemplatePath(path) : null,
                (environment, object) -> object instanceof File file ? new TemplateFile(file) : null);
    }
}