Step1 - tests

✅ Tests Créés avec Succès

📊 Statistiques

  • 110 tests au total
  • 100% de succès ✅
  • 3 fichiers de tests créés
  • ~95% de coverage des méthodes publiques

📁 Fichiers Créés

  1. FileBuiltinTest.java (24 tests) - Teste toutes les méthodes de FileBuiltin - Couvre : name, base_name, paths absolus/relatifs, directories - Cas particuliers : fichiers avec points multiples, caractères spéciaux, espaces
  2. BidjiBuiltInTest.java (55 tests) - Gestion des lignes (lines, line) - Lecture de fichiers (5 variantes différentes) - Utilitaires de chemins - Analyse d’occurrences - Exécution système (exec) - Méthodes dépréciées
  3. PageBuiltinTest.java (31 tests) - id, title, original_title - Gestion du contenu - addLine, hasLines - Unicode et caractères spéciaux
  4. README.md - Documentation complète - Vue d’ensemble de tous les tests - Comportements documentés - Points d’attention pour le refactoring - Instructions d’exécution

🔍 Découvertes Importantes

  1. base_name() retire TOUT après le premier point “My.File.With.Dots.txt” → “My” // Pas “My.File.With.Dots” !
  2. Duplication entre FileBuiltin et BidjiBuiltIn - BidjiBuiltIn a des méthodes @Deprecated qui dupliquent FileBuiltin
  3. BidjiBuiltIn a trop de responsabilités - Mélange lignes, fichiers, utils généraux, analyse de données

🚀 Prochaines Étapes

Ces tests serviront de baseline pour :

  1. Valider que les Custom Directives FreeMarker reproduisent le même comportement
  2. Tests de non-régression pendant le refactoring
  3. Documentation du comportement actuel

Tu peux maintenant passer à l’implémentation des Custom Directives en toute confiance ! 🎯

Step2 - Phase 3 Implémentation bjio

✅ TERMINÉ - Voir doc/Phase3-Bjio-Implementation.md pour les détails complets

Résumé :

  • ✅ Namespace bjio créé avec 9 méthodes
  • ✅ 29 tests pour BjioNamespace (100% pass)
  • ✅ FileBuiltin déprécié avec exceptions instructives
  • ✅ 21 tests pour FileBuiltin (validation des exceptions)
  • ✅ Script find_builtin.sh pour trouver les usages
  • ✅ Documentation complète de la Phase 3

Nouvelle syntaxe :

${bjio.name(file)}              au lieu de  ${file?api.name()}
${bjio.base_name(file)}         au lieu de  ${file?api.base_name()}
${bjio.absolute_path(file)}     au lieu de  ${file?api.absolute_path()}
${bjio.relative_path(file)}     au lieu de  ${file?api.relative_path()}
${bjio.path(file, 'relative')}  au lieu de  ${file?api.path('relative')}

Commandes :

# Tests
mvn test -Dtest=BjioNamespaceTest,FileBuiltinTest
# Résultat: Tests run: 50, Failures: 0, Errors: 0 ✅

# Recherche d'usages
./find_builtin.sh
# Génère: find_builtin.res.txt

Step3 - Documentation Originale

Refactoring des Builtins avec FreeMarker Custom Directives

Date: 2026-02-07 Status: Proposition pour tests Phase 3 Objectif: Remplacer l’approche actuelle des builtins par des Custom Directives FreeMarker natives


Table des Matières

  1. Analyse de l’Existant
  2. Proposition d’Architecture
  3. Exemples de Code
  4. Plan d’Implémentation
  5. Migration Progressive
  6. Tests

🔍 Analyse de l’Existant

Architecture Actuelle

Les builtins sont actuellement implémentés comme des Java Beans instanciés et injectés dans le contexte FreeMarker :

// Dans BidjicTask.java
engine.assign("bj", new BidjiBuiltIn());
engine.assign("js", new JavascriptBuiltin());

// Dans BidjiTxtTask.java
bidjicTask.assign("file", new FileBuiltin(inputFile));
bidjicTask.assign(VAR_NAME, helper);  // txtt, csvt, etc.

Utilisation dans les Templates

[#-- Accès aux méthodes via les beans --]
${file?api.name()}
${file?api.base_name()}
${file?api.absolute_path()}
${file?api.relative_path()}

[#-- Pour les helpers --]
[#foreach line in txtt?api.lines()]
    ${line.content}
[/#foreach]

[#-- Méthodes utilitaires --]
${bj?api.read_file('path/to/file')}
${bj?api.filename('path/to/file')}

Hiérarchie des Builtins

BidjiBuiltIn<T>
├── FileBuiltin
├── PageBuiltin<T>
├── JavascriptBuiltin
├── CppBuiltIn
└── [Helpers]
    ├── TxtHelper extends BidjiBuiltIn<TxtLine>
    ├── CsvHelper extends BidjiBuiltIn<CsvLine>
    ├── JsontHelper extends BidjiBuiltIn
    └── ...

Problèmes Identifiés

❌ Syntaxe non standard : file?api.name() n’est pas une syntaxe FreeMarker native ❌ Pas de validation : Erreurs à runtime difficiles à comprendre ❌ Auto-complétion impossible : Les IDEs ne peuvent pas suggérer les méthodes disponibles ❌ Duplication de code : BidjiBuiltIn contient des méthodes dépréciées qui dupliquent FileBuiltin ❌ Mélange de responsabilités : BidjiBuiltIn fait à la fois helper de lignes et utilitaires de fichiers


🎯 Proposition d’Architecture

Option A : Namespace Functions (Recommandé)

Principe : Créer des namespaces FreeMarker avec des méthodes comme pour bidji.entity.*

[#-- AVANT --]
${file?api.name()}
${file?api.base_name()}

[#-- APRÈS --]
${bidji.file.name(file)}
${bidji.file.base_name(file)}

Avantages :

  • ✅ Syntaxe cohérente avec la proposition Phase 3 pour entity/property
  • ✅ Namespace clair et organisé
  • ✅ Facile à documenter et à tester
  • ✅ Validation native par FreeMarker

Option B : Custom Directives Block (Alternative)

Principe : Créer des directives de type [@bidji.with_file]

[#-- AVANT --]
${file?api.name()}
${file?api.base_name()}

[#-- APRÈS --]
[@bidji.with_file path=inputFile; f]
    ${f.name}
    ${f.base_name}
    ${f.absolute_path}
[/@bidji.with_file]

Avantages :

  • ✅ Context scope limité
  • ✅ Plus lisible pour de multiples accès

Inconvénients :

  • ⚠️ Plus verbeux pour un seul accès
  • ⚠️ Moins flexible

Architecture Proposée (Option A)

org.bidji.freemarker/
├── BidjiFreeMarkerDirectives.java      [Existing - from Phase 3]
├── builtin/
│   ├── FileNamespace.java               [NEW]
│   ├── UtilsNamespace.java              [NEW]
│   ├── LinesNamespace.java              [NEW]
│   └── JavascriptNamespace.java         [NEW]
└── BidjiFreeMarkerModel.java            [ENHANCED]

Structure de namespaces :

bidji/
├── file/               # Opérations sur fichiers (remplace FileBuiltin)
│   ├── name(file)
│   ├── base_name(file)
│   ├── absolute_path(file)
│   ├── relative_path(file)
│   ├── dir_name(file)
│   ├── absolute_dir_path(file)
│   └── relative_dir_path(file)
│
├── utils/              # Utilitaires généraux (remplace BidjiBuiltIn méthodes statiques)
│   ├── read_file(path)
│   ├── read_file_without_eol(path)
│   ├── read_file_with_lf(path)
│   ├── read_file_with_crlf(path)
│   ├── filename(path)
│   ├── basename(path)
│   ├── absolute_file_path(path)
│   ├── relative_file_path(path)
│   ├── absolute_dir_path(path)
│   └── relative_dir_path(path)
│
├── lines/              # Opérations sur collections de lignes (remplace BidjiBuiltIn<T>)
│   ├── all(helper)
│   ├── get(helper, index)
│   └── count(helper)
│
├── js/                 # Helpers JavaScript (remplace JavascriptBuiltin)
│   ├── escape(str)
│   └── ...
│
└── cpp/                # Helpers C++ (remplace CppBuiltIn)
    ├── ...
    └── ...

📝 Exemples de Code

Exemple 1 : FileNamespace

Implémentation Java :

package org.bidji.freemarker.builtin;

import freemarker.template.*;
import java.io.File;
import java.util.List;
import net.sourceforge.ant4x.util.FileHelper;

/**
 * FreeMarker namespace for file operations.
 * Replaces FileBuiltin class.
 */
public class FileNamespace implements TemplateHashModel {

    @Override
    public TemplateModel get(String key) throws TemplateModelException {
        switch (key) {
            case "name":
                return new FileNameMethod();
            case "base_name":
                return new FileBaseNameMethod();
            case "absolute_path":
                return new FileAbsolutePathMethod();
            case "relative_path":
                return new FileRelativePathMethod();
            case "path":
                return new FilePathMethod();
            case "dir_name":
                return new FileDirNameMethod();
            case "absolute_dir_path":
                return new FileAbsoluteDirPathMethod();
            case "relative_dir_path":
                return new FileRelativeDirPathMethod();
            case "dir_path":
                return new FileDirPathMethod();
            default:
                throw new TemplateModelException("Unknown file method: " + key);
        }
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

    // =========================================================================
    // FILE NAME METHODS
    // =========================================================================

    /**
     * bidji.file.name(file) - Returns the file name
     */
    private static class FileNameMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException(
                    "file.name requires 1 argument: file.name(file)"
                );
            }
            File file = unwrapFile(arguments.get(0));
            return file.getName();
        }
    }

    /**
     * bidji.file.base_name(file) - Returns the base name (without extension)
     */
    private static class FileBaseNameMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException(
                    "file.base_name requires 1 argument: file.base_name(file)"
                );
            }
            File file = unwrapFile(arguments.get(0));
            return FileHelper.getBaseName(file);
        }
    }

    /**
     * bidji.file.absolute_path(file) - Returns the absolute path
     */
    private static class FileAbsolutePathMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException(
                    "file.absolute_path requires 1 argument: file.absolute_path(file)"
                );
            }
            File file = unwrapFile(arguments.get(0));
            return file.getAbsolutePath();
        }
    }

    /**
     * bidji.file.relative_path(file) - Returns the relative path
     */
    private static class FileRelativePathMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException(
                    "file.relative_path requires 1 argument: file.relative_path(file)"
                );
            }
            File file = unwrapFile(arguments.get(0));
            return FileHelper.truncatePath(file);
        }
    }

    /**
     * bidji.file.path(file, mode) - Returns path (absolute or relative based on mode)
     * @param mode "relative" or "absolute" (default)
     */
    private static class FilePathMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() < 1 || arguments.size() > 2) {
                throw new TemplateModelException(
                    "file.path requires 1 or 2 arguments: file.path(file) or file.path(file, mode)"
                );
            }
            File file = unwrapFile(arguments.get(0));
            String mode = "absolute";
            if (arguments.size() == 2) {
                mode = unwrapString(arguments.get(1));
            }

            if ("relative".equals(mode)) {
                return FileHelper.truncatePath(file);
            }
            return file.getAbsolutePath();
        }
    }

    // =========================================================================
    // DIRECTORY METHODS
    // =========================================================================

    /**
     * bidji.file.dir_name(file) - Returns the parent directory name
     */
    private static class FileDirNameMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException(
                    "file.dir_name requires 1 argument: file.dir_name(file)"
                );
            }
            File file = unwrapFile(arguments.get(0));
            return FileHelper.getBaseName(file.getParentFile());
        }
    }

    /**
     * bidji.file.absolute_dir_path(file) - Returns the absolute path of parent directory
     */
    private static class FileAbsoluteDirPathMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException(
                    "file.absolute_dir_path requires 1 argument: file.absolute_dir_path(file)"
                );
            }
            File file = unwrapFile(arguments.get(0));
            return file.getParentFile().getAbsolutePath();
        }
    }

    /**
     * bidji.file.relative_dir_path(file) - Returns the relative path of parent directory
     */
    private static class FileRelativeDirPathMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException(
                    "file.relative_dir_path requires 1 argument: file.relative_dir_path(file)"
                );
            }
            File file = unwrapFile(arguments.get(0));
            return FileHelper.truncatePath(file.getParentFile());
        }
    }

    /**
     * bidji.file.dir_path(file, mode) - Returns directory path (absolute or relative)
     */
    private static class FileDirPathMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() < 1 || arguments.size() > 2) {
                throw new TemplateModelException(
                    "file.dir_path requires 1 or 2 arguments: file.dir_path(file) or file.dir_path(file, mode)"
                );
            }
            File file = unwrapFile(arguments.get(0));
            String mode = "absolute";
            if (arguments.size() == 2) {
                mode = unwrapString(arguments.get(1));
            }

            if ("relative".equals(mode)) {
                return FileHelper.truncatePath(file.getParentFile());
            }
            return file.getParentFile().getAbsolutePath();
        }
    }

    // =========================================================================
    // UTILITIES
    // =========================================================================

    private static File unwrapFile(Object arg) throws TemplateModelException {
        // Try to unwrap as File directly
        if (arg instanceof File) {
            return (File) arg;
        }

        // Try to unwrap from FreeMarker wrapper
        Object unwrapped = freemarker.template.utility.DeepUnwrap.unwrap((TemplateModel) arg);
        if (unwrapped instanceof File) {
            return (File) unwrapped;
        }

        throw new TemplateModelException(
            "Argument is not a File: " + (unwrapped != null ? unwrapped.getClass().getName() : "null")
        );
    }

    private static String unwrapString(Object arg) throws TemplateModelException {
        if (arg instanceof TemplateScalarModel) {
            return ((TemplateScalarModel) arg).getAsString();
        }
        Object unwrapped = freemarker.template.utility.DeepUnwrap.unwrap((TemplateModel) arg);
        if (unwrapped instanceof String) {
            return (String) unwrapped;
        }
        throw new TemplateModelException("Argument is not a String");
    }
}

Exemple 2 : UtilsNamespace

Implémentation Java :

package org.bidji.freemarker.builtin;

import freemarker.template.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.List;
import org.apache.tools.ant.BuildException;
import net.sourceforge.ant4x.util.FileHelper;
import net.sourceforge.ant4x.util.StringHelper;

/**
 * FreeMarker namespace for utility functions.
 * Replaces static utility methods from BidjiBuiltIn.
 */
public class UtilsNamespace implements TemplateHashModel {

    @Override
    public TemplateModel get(String key) throws TemplateModelException {
        switch (key) {
            case "read_file":
                return new ReadFileMethod();
            case "read_file_without_eol":
                return new ReadFileWithoutEolMethod();
            case "read_file_with_lf":
                return new ReadFileWithLfMethod();
            case "read_file_with_crlf":
                return new ReadFileWithCrlfMethod();
            case "read_file_backspace_eol":
                return new ReadFileBackspaceEolMethod();
            case "filename":
                return new FilenameMethod();
            case "basename":
                return new BasenameMethod();
            case "absolute_file_path":
                return new AbsoluteFilePathMethod();
            case "relative_file_path":
                return new RelativeFilePathMethod();
            case "absolute_dir_path":
                return new AbsoluteDirPathMethod();
            case "relative_dir_path":
                return new RelativeDirPathMethod();
            case "exec":
                return new ExecMethod();
            default:
                throw new TemplateModelException("Unknown utils method: " + key);
        }
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

    // =========================================================================
    // FILE READING METHODS
    // =========================================================================

    /**
     * bidji.utils.read_file(path) - Reads entire file content
     */
    private static class ReadFileMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException(
                    "utils.read_file requires 1 argument: utils.read_file(path)"
                );
            }
            String path = unwrapString(arguments.get(0));
            if (path == null || path.trim().isEmpty()) {
                throw new TemplateModelException("read_file: undefined path");
            }
            File f = new File(path);
            if (!f.exists()) {
                throw new TemplateModelException("read_file: file not found: " + f.getAbsolutePath());
            }
            return FileHelper.getBuffer(f).toString();
        }
    }

    /**
     * bidji.utils.read_file_without_eol(path) - Reads file and removes EOL + comments
     */
    private static class ReadFileWithoutEolMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException(
                    "utils.read_file_without_eol requires 1 argument"
                );
            }
            String path = unwrapString(arguments.get(0));
            if (path == null || path.trim().isEmpty()) {
                throw new TemplateModelException("read_file_without_eol: undefined path");
            }
            File f = new File(path);
            if (!f.exists()) {
                throw new TemplateModelException("read_file_without_eol: file not found: " + f.getAbsolutePath());
            }

            StringBuffer res = new StringBuffer();
            List<String> lines = StringHelper.splitByEol(FileHelper.getBuffer(f).toString());
            for (String line : lines) {
                res.append(line.trim());
            }

            // Remove comments (/* */ and //)
            return res.toString().replaceAll("(?:/\\*(?:[^*]|(?:\\*+[^*/]))*\\*+/)|(?://.*)", "");
        }
    }

    /**
     * bidji.utils.read_file_with_lf(path) - Reads file with Unix line endings
     */
    private static class ReadFileWithLfMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException(
                    "utils.read_file_with_lf requires 1 argument"
                );
            }
            String path = unwrapString(arguments.get(0));
            if (path == null || path.trim().isEmpty()) {
                throw new TemplateModelException("read_file_with_lf: undefined path");
            }
            File f = new File(path);
            if (!f.exists()) {
                throw new TemplateModelException("read_file_with_lf: file not found: " + f.getAbsolutePath());
            }

            StringBuffer res = new StringBuffer();
            List<String> lines = StringHelper.splitByEol(FileHelper.getBuffer(f).toString());
            for (String line : lines) {
                res.append(line.trim() + "\\n");
            }
            return res.toString();
        }
    }

    /**
     * bidji.utils.read_file_with_crlf(path) - Reads file with Windows line endings
     */
    private static class ReadFileWithCrlfMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException(
                    "utils.read_file_with_crlf requires 1 argument"
                );
            }
            String path = unwrapString(arguments.get(0));
            if (path == null || path.trim().isEmpty()) {
                throw new TemplateModelException("read_file_with_crlf: undefined path");
            }
            File f = new File(path);
            if (!f.exists()) {
                throw new TemplateModelException("read_file_with_crlf: file not found: " + f.getAbsolutePath());
            }

            StringBuffer res = new StringBuffer();
            List<String> lines = StringHelper.splitByEol(FileHelper.getBuffer(f).toString());
            for (String line : lines) {
                res.append(line.trim() + org.bidji.Var.EOL);
            }
            return res.toString();
        }
    }

    /**
     * bidji.utils.read_file_backspace_eol(path) - Reads file with backslash line continuations
     */
    private static class ReadFileBackspaceEolMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException(
                    "utils.read_file_backspace_eol requires 1 argument"
                );
            }
            String path = unwrapString(arguments.get(0));
            if (path == null || path.trim().isEmpty()) {
                throw new TemplateModelException("read_file_backspace_eol: undefined path");
            }
            File f = new File(path);
            if (!f.exists()) {
                throw new TemplateModelException("read_file_backspace_eol: file not found: " + f.getAbsolutePath());
            }

            StringBuffer res = new StringBuffer();
            List<String> lines = StringHelper.splitByEol(FileHelper.getBuffer(f).toString());
            for (String line : lines) {
                res.append(line + "\\\n");
            }
            return res.toString();
        }
    }

    // =========================================================================
    // PATH UTILITY METHODS
    // =========================================================================

    /**
     * bidji.utils.filename(path) - Returns filename from path string
     */
    private static class FilenameMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException("utils.filename requires 1 argument");
            }
            String path = unwrapString(arguments.get(0));
            if (path == null || path.trim().isEmpty()) {
                throw new TemplateModelException("filename: undefined path");
            }
            return new File(path).getName();
        }
    }

    /**
     * bidji.utils.basename(path) - Returns basename (without extension) from path string
     */
    private static class BasenameMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException("utils.basename requires 1 argument");
            }
            String path = unwrapString(arguments.get(0));
            if (path == null || path.trim().isEmpty()) {
                throw new TemplateModelException("basename: undefined path");
            }
            return FileHelper.getBaseName(new File(path));
        }
    }

    /**
     * bidji.utils.absolute_file_path(path) - Returns absolute file path
     */
    private static class AbsoluteFilePathMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException("utils.absolute_file_path requires 1 argument");
            }
            String path = unwrapString(arguments.get(0));
            if (path == null || path.trim().isEmpty()) {
                throw new TemplateModelException("absolute_file_path: undefined path");
            }
            return new File(path).getAbsolutePath();
        }
    }

    /**
     * bidji.utils.relative_file_path(path) - Returns relative file path
     */
    private static class RelativeFilePathMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException("utils.relative_file_path requires 1 argument");
            }
            String path = unwrapString(arguments.get(0));
            if (path == null || path.trim().isEmpty()) {
                throw new TemplateModelException("relative_file_path: undefined path");
            }
            return FileHelper.truncatePath(new File(path));
        }
    }

    /**
     * bidji.utils.absolute_dir_path(path) - Returns absolute directory path
     */
    private static class AbsoluteDirPathMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException("utils.absolute_dir_path requires 1 argument");
            }
            String path = unwrapString(arguments.get(0));
            if (path == null || path.trim().isEmpty()) {
                throw new TemplateModelException("absolute_dir_path: undefined path");
            }
            File f = new File(path);
            if (f.isDirectory()) {
                return f.getAbsolutePath();
            }
            return f.getParentFile().getAbsolutePath();
        }
    }

    /**
     * bidji.utils.relative_dir_path(path) - Returns relative directory path
     */
    private static class RelativeDirPathMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException("utils.relative_dir_path requires 1 argument");
            }
            String path = unwrapString(arguments.get(0));
            if (path == null || path.trim().isEmpty()) {
                throw new TemplateModelException("relative_dir_path: undefined path");
            }
            File f = new File(path);
            if (f.isDirectory()) {
                return FileHelper.truncatePath(f);
            }
            return FileHelper.truncatePath(f.getParentFile());
        }
    }

    // =========================================================================
    // SYSTEM EXECUTION
    // =========================================================================

    /**
     * bidji.utils.exec(dir, cmd) - Executes shell command
     */
    private static class ExecMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 2) {
                throw new TemplateModelException(
                    "utils.exec requires 2 arguments: utils.exec(dir, cmd)"
                );
            }
            String dir = unwrapString(arguments.get(0));
            String cmd = unwrapString(arguments.get(1));

            try {
                ProcessBuilder builder = new ProcessBuilder();
                builder.command("sh", "-c", cmd);
                builder.directory(new File(dir));
                Process process = builder.start();
                return org.apache.commons.io.IOUtils.toString(
                    process.getInputStream(),
                    java.nio.charset.StandardCharsets.UTF_8.name()
                );
            } catch (java.io.IOException e) {
                throw new TemplateModelException("exec failed: " + e.getMessage(), e);
            }
        }
    }

    // =========================================================================
    // UTILITIES
    // =========================================================================

    private static String unwrapString(Object arg) throws TemplateModelException {
        if (arg instanceof TemplateScalarModel) {
            return ((TemplateScalarModel) arg).getAsString();
        }
        Object unwrapped = freemarker.template.utility.DeepUnwrap.unwrap((TemplateModel) arg);
        if (unwrapped instanceof String) {
            return (String) unwrapped;
        }
        throw new TemplateModelException("Argument is not a String");
    }
}

Exemple 3 : LinesNamespace

Implémentation Java :

package org.bidji.freemarker.builtin;

import freemarker.template.*;
import java.util.List;
import org.bidji.builtin.BidjiBuiltIn;

/**
 * FreeMarker namespace for line operations.
 * Replaces BidjiBuiltIn<T> methods for accessing lines.
 */
public class LinesNamespace implements TemplateHashModel {

    @Override
    public TemplateModel get(String key) throws TemplateModelException {
        switch (key) {
            case "all":
                return new AllLinesMethod();
            case "get":
                return new GetLineMethod();
            case "count":
                return new CountLinesMethod();
            default:
                throw new TemplateModelException("Unknown lines method: " + key);
        }
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

    /**
     * bidji.lines.all(helper) - Returns all lines
     * Replaces: txtt?api.lines()
     */
    private static class AllLinesMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException(
                    "lines.all requires 1 argument: lines.all(helper)"
                );
            }
            Object unwrapped = freemarker.template.utility.DeepUnwrap.unwrap(
                (TemplateModel) arguments.get(0)
            );
            if (!(unwrapped instanceof BidjiBuiltIn)) {
                throw new TemplateModelException(
                    "Argument is not a BidjiBuiltIn helper: " + unwrapped.getClass().getName()
                );
            }
            BidjiBuiltIn<?> helper = (BidjiBuiltIn<?>) unwrapped;
            return helper.lines();
        }
    }

    /**
     * bidji.lines.get(helper, index) - Returns line at index (1-based)
     * Replaces: txtt?api.line(i)
     */
    private static class GetLineMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 2) {
                throw new TemplateModelException(
                    "lines.get requires 2 arguments: lines.get(helper, index)"
                );
            }

            Object unwrappedHelper = freemarker.template.utility.DeepUnwrap.unwrap(
                (TemplateModel) arguments.get(0)
            );
            if (!(unwrappedHelper instanceof BidjiBuiltIn)) {
                throw new TemplateModelException(
                    "First argument is not a BidjiBuiltIn helper"
                );
            }

            int index;
            Object indexArg = arguments.get(1);
            if (indexArg instanceof TemplateNumberModel) {
                index = ((TemplateNumberModel) indexArg).getAsNumber().intValue();
            } else {
                throw new TemplateModelException("Second argument must be a number");
            }

            BidjiBuiltIn<?> helper = (BidjiBuiltIn<?>) unwrappedHelper;
            return helper.line(index);
        }
    }

    /**
     * bidji.lines.count(helper) - Returns number of lines
     */
    private static class CountLinesMethod implements TemplateMethodModelEx {
        @Override
        public Object exec(List arguments) throws TemplateModelException {
            if (arguments.size() != 1) {
                throw new TemplateModelException(
                    "lines.count requires 1 argument: lines.count(helper)"
                );
            }
            Object unwrapped = freemarker.template.utility.DeepUnwrap.unwrap(
                (TemplateModel) arguments.get(0)
            );
            if (!(unwrapped instanceof BidjiBuiltIn)) {
                throw new TemplateModelException(
                    "Argument is not a BidjiBuiltIn helper"
                );
            }
            BidjiBuiltIn<?> helper = (BidjiBuiltIn<?>) unwrapped;
            return helper.lines().size();
        }
    }
}

Exemple 4 : Intégration dans BidjiFreeMarkerModel

Modification de BidjiFreeMarkerModel.java :

package org.bidji.freemarker;

import freemarker.template.*;
import org.bidji.freemarker.builtin.*;
import org.bidji.util.ModelHelper;

/**
 * Root namespace for all Bidji FreeMarker directives and functions.
 */
public class BidjiFreeMarkerModel implements TemplateHashModel {
    private final ModelHelper modelHelper;

    public BidjiFreeMarkerModel(ModelHelper helper) {
        this.modelHelper = helper;
    }

    @Override
    public TemplateModel get(String key) throws TemplateModelException {
        switch (key) {
            // Phase 3 namespaces (entity, property, ui, etc.)
            case "entity":
                return new EntityNamespaceModel(modelHelper);
            case "property":
                return new PropertyNamespaceModel(modelHelper);
            case "ui":
                return new UINamespaceModel(modelHelper);
            case "relation":
                return new RelationNamespaceModel(modelHelper);
            case "model":
                return new ModelNamespaceModel(modelHelper);

            // NEW: Builtin namespaces
            case "file":
                return new FileNamespace();
            case "utils":
                return new UtilsNamespace();
            case "lines":
                return new LinesNamespace();
            case "js":
                return new JavascriptNamespace();
            case "cpp":
                return new CppNamespace();

            default:
                throw new TemplateModelException("Unknown bidji namespace: " + key);
        }
    }

    @Override
    public boolean isEmpty() {
        return false;
    }
}

🛠️ Plan d’Implémentation

Phase 1 : Création des Namespaces (2-3 jours)

  1. Créer le package org.bidji.freemarker.builtin

  2. Implémenter les namespaces :

    • FileNamespace.java ✅ (exemple complet ci-dessus)
    • UtilsNamespace.java ✅ (exemple complet ci-dessus)
    • LinesNamespace.java ✅ (exemple complet ci-dessus)
    • JavascriptNamespace.java (à implémenter)
    • CppNamespace.java (à implémenter)
  3. Intégrer dans BidjiFreeMarkerModel ✅

Phase 2 : Tests Unitaires (1-2 jours)

Créer BidjiFreeMarkerBuiltinTest.java :

package org.bidji.freemarker.builtin;

import org.junit.Test;
import static org.junit.Assert.*;
import freemarker.template.*;
import java.io.File;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;

public class BidjiFreeMarkerBuiltinTest {

    @Test
    public void testFileNameNamespace() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);

        // Register bidji namespace
        BidjiFreeMarkerModel bidjiModel = new BidjiFreeMarkerModel(null);
        cfg.setSharedVariable("bidji", bidjiModel);

        // Create template
        String templateContent = "${bidji.file.name(testFile)}";
        Template template = new Template("test", templateContent, cfg);

        // Prepare data model
        Map<String, Object> dataModel = new HashMap<>();
        dataModel.put("testFile", new File("/path/to/myfile.txt"));

        // Process template
        StringWriter writer = new StringWriter();
        template.process(dataModel, writer);

        assertEquals("myfile.txt", writer.toString());
    }

    @Test
    public void testFileBaseNameNamespace() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
        BidjiFreeMarkerModel bidjiModel = new BidjiFreeMarkerModel(null);
        cfg.setSharedVariable("bidji", bidjiModel);

        String templateContent = "${bidji.file.base_name(testFile)}";
        Template template = new Template("test", templateContent, cfg);

        Map<String, Object> dataModel = new HashMap<>();
        dataModel.put("testFile", new File("/path/to/myfile.txt"));

        StringWriter writer = new StringWriter();
        template.process(dataModel, writer);

        assertEquals("myfile", writer.toString());
    }

    @Test
    public void testUtilsFilename() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
        BidjiFreeMarkerModel bidjiModel = new BidjiFreeMarkerModel(null);
        cfg.setSharedVariable("bidji", bidjiModel);

        String templateContent = "${bidji.utils.filename('/path/to/myfile.txt')}";
        Template template = new Template("test", templateContent, cfg);

        Map<String, Object> dataModel = new HashMap<>();

        StringWriter writer = new StringWriter();
        template.process(dataModel, writer);

        assertEquals("myfile.txt", writer.toString());
    }

    @Test
    public void testFilePathWithMode() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
        BidjiFreeMarkerModel bidjiModel = new BidjiFreeMarkerModel(null);
        cfg.setSharedVariable("bidji", bidjiModel);

        String templateContent = "${bidji.file.path(testFile, 'relative')}";
        Template template = new Template("test", templateContent, cfg);

        Map<String, Object> dataModel = new HashMap<>();
        dataModel.put("testFile", new File("/home/user/project/file.txt"));

        StringWriter writer = new StringWriter();
        template.process(dataModel, writer);

        // Should return truncated path
        assertTrue(writer.toString().contains("file.txt"));
    }

    @Test(expected = TemplateException.class)
    public void testFileNameInvalidArguments() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
        BidjiFreeMarkerModel bidjiModel = new BidjiFreeMarkerModel(null);
        cfg.setSharedVariable("bidji", bidjiModel);

        // Test with no arguments (should fail)
        String templateContent = "${bidji.file.name()}";
        Template template = new Template("test", templateContent, cfg);

        Map<String, Object> dataModel = new HashMap<>();
        StringWriter writer = new StringWriter();
        template.process(dataModel, writer); // Should throw exception
    }
}

🔄 Migration Progressive

Étape 1 : Support Dual (Backward Compatibility)

Garder les anciennes classes FileBuiltin, BidjiBuiltIn, etc. mais les marquer @Deprecated :

/**
 * @deprecated Use bidji.file.* namespace methods instead
 * Example: Instead of ${file?api.name()}, use ${bidji.file.name(file)}
 */
@Deprecated
public class FileBuiltin {
    // ... existing code
}

Dans BidjicTask, continuer d’assigner les deux :

// Legacy support
engine.assign("file", new FileBuiltin(inputFile));
engine.assign("bj", new BidjiBuiltIn());

// New namespace support
BidjiFreeMarkerModel bidjiModel = new BidjiFreeMarkerModel(modelHelper);
engine.assign("bidji", bidjiModel);

Étape 2 : Migration des Templates

Tableau de correspondance :

Ancienne Syntaxe Nouvelle Syntaxe
${file?api.name()} ${bidji.file.name(file)}
${file?api.base_name()} ${bidji.file.base_name(file)}
${file?api.absolute_path()} ${bidji.file.absolute_path(file)}
${file?api.relative_path()} ${bidji.file.relative_path(file)}
${file?api.path('relative')} ${bidji.file.path(file, 'relative')}
${txtt?api.lines()} ${bidji.lines.all(txtt)}
${txtt?api.line(i)} ${bidji.lines.get(txtt, i)}
${bj?api.read_file(path)} ${bidji.utils.read_file(path)}
${bj?api.filename(path)} ${bidji.utils.filename(path)}
${bj?api.basename(path)} ${bidji.utils.basename(path)}

Script de migration automatique :

#!/bin/bash
# migrate-builtin-syntax.sh

# Remplacer file?api.* par bidji.file.*
sed -i 's/\${file?api\.name()}/\${bidji.file.name(file)}/g' templates/**/*.ftl
sed -i 's/\${file?api\.base_name()}/\${bidji.file.base_name(file)}/g' templates/**/*.ftl
sed -i 's/\${file?api\.absolute_path()}/\${bidji.file.absolute_path(file)}/g' templates/**/*.ftl
sed -i 's/\${file?api\.relative_path()}/\${bidji.file.relative_path(file)}/g' templates/**/*.ftl

# Remplacer bj?api.* par bidji.utils.*
sed -i 's/\${bj?api\.read_file(\([^)]*\))}/\${bidji.utils.read_file(\1)}/g' templates/**/*.ftl
sed -i 's/\${bj?api\.filename(\([^)]*\))}/\${bidji.utils.filename(\1)}/g' templates/**/*.ftl

# Remplacer txtt?api.lines() par bidji.lines.all(txtt)
sed -i 's/txtt?api\.lines()/bidji.lines.all(txtt)/g' templates/**/*.ftl
sed -i 's/csvt?api\.lines()/bidji.lines.all(csvt)/g' templates/**/*.ftl

echo "Migration completed. Please review changes manually."

Étape 3 : Dépréciation Progressive

Timeline suggérée :

  1. Release N : Support dual - anciennes et nouvelles syntaxes fonctionnent
  2. Release N+1 : Warnings de dépréciation dans les logs
  3. Release N+2 : Documentation updated - nouvelle syntaxe recommandée
  4. Release N+3 : Suppression du support legacy

✅ Tests

Tests d’Intégration

Créer templates de test dans src/test/resources/templates/builtin/ :

test-file-namespace.ftl :

[#-- Test file namespace --]
File name: ${bidji.file.name(testFile)}
Base name: ${bidji.file.base_name(testFile)}
Absolute path: ${bidji.file.absolute_path(testFile)}
Relative path: ${bidji.file.relative_path(testFile)}
Dir name: ${bidji.file.dir_name(testFile)}
Absolute dir: ${bidji.file.absolute_dir_path(testFile)}
Relative dir: ${bidji.file.relative_dir_path(testFile)}

test-utils-namespace.ftl :

[#-- Test utils namespace --]
Filename from path: ${bidji.utils.filename('/path/to/file.txt')}
Basename from path: ${bidji.utils.basename('/path/to/file.txt')}
[#assign content = bidji.utils.read_file('test-data.txt')]
File content: ${content}

test-lines-namespace.ftl :

[#-- Test lines namespace --]
Total lines: ${bidji.lines.count(txtt)}
[#foreach line in bidji.lines.all(txtt)]
Line ${line?index + 1}: ${line.content}
[/#foreach]

Get line 5: ${bidji.lines.get(txtt, 5).content}

Tests de Performance

Comparer ancienne vs nouvelle approche :

@Test
public void testPerformanceComparison() throws Exception {
    // OLD: FileBuiltin
    long start1 = System.currentTimeMillis();
    FileBuiltin oldBuiltin = new FileBuiltin(testFile);
    for (int i = 0; i < 10000; i++) {
        String name = oldBuiltin.name();
    }
    long time1 = System.currentTimeMillis() - start1;

    // NEW: FileNamespace
    Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
    BidjiFreeMarkerModel bidjiModel = new BidjiFreeMarkerModel(null);
    cfg.setSharedVariable("bidji", bidjiModel);

    String templateContent = "${bidji.file.name(testFile)}";
    Template template = new Template("test", templateContent, cfg);

    long start2 = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) {
        Map<String, Object> dataModel = new HashMap<>();
        dataModel.put("testFile", testFile);
        StringWriter writer = new StringWriter();
        template.process(dataModel, writer);
    }
    long time2 = System.currentTimeMillis() - start2;

    System.out.println("OLD approach: " + time1 + "ms");
    System.out.println("NEW approach: " + time2 + "ms");

    // Acceptable if new approach is < 2x slower
    assertTrue("Performance regression too high", time2 < time1 * 2);
}

📊 Comparaison Avant/Après

Exemple Complet : Template de Génération

AVANT (syntaxe actuelle) :

[#-- Generate Java class from text file --]
package com.example.${bidji.utils.basename(file?api.absolute_path())};

import java.util.*;

/**
 * Generated from: ${file?api.name()}
 * Path: ${file?api.relative_path()}
 */
public class ${bidji.utils.basename(file?api.name())?cap_first}Data {

    [#assign content = bj?api.read_file(file?api.absolute_path())]

    // Total lines: ${txtt?api.lines()?size}

    private static final String[] DATA = {
        [#foreach line in txtt?api.lines()]
        "${line.content}"[#if line?has_next],[/#if]
        [/#foreach]
    };

    public static String getLine(int index) {
        return DATA[index - 1];
    }
}

APRÈS (nouvelle syntaxe) :

[#-- Generate Java class from text file --]
package com.example.${bidji.utils.basename(bidji.file.absolute_path(file))};

import java.util.*;

/**
 * Generated from: ${bidji.file.name(file)}
 * Path: ${bidji.file.relative_path(file)}
 */
public class ${bidji.utils.basename(bidji.file.name(file))?cap_first}Data {

    [#assign content = bidji.utils.read_file(bidji.file.absolute_path(file))]

    // Total lines: ${bidji.lines.count(txtt)}

    private static final String[] DATA = {
        [#foreach line in bidji.lines.all(txtt)]
        "${line.content}"[#if line?has_next],[/#if]
        [/#foreach]
    };

    public static String getLine(int index) {
        return DATA[index - 1];
    }
}

Avantages de la nouvelle syntaxe :

  • ✅ Plus explicite : on voit clairement qu’on appelle bidji.file.name(file) avec file en paramètre
  • ✅ Cohérent : même syntaxe que bidji.entity.name(entity) de Phase 3
  • ✅ Validé par FreeMarker : erreurs claires si mauvais nombre/type d’arguments
  • ✅ Auto-documenté : namespace bidji.file.* indique clairement qu’on manipule des fichiers

🎯 Recommandations

Avantages de l’Approche Custom Directives

  1. Cohérence avec Phase 3 : même pattern pour entity/property et file/utils
  2. Validation native : FreeMarker valide les arguments automatiquement
  3. Meilleure documentation : namespace clair bidji.file.*, bidji.utils.*
  4. Extensibilité : facile d’ajouter de nouvelles méthodes
  5. Testabilité : chaque namespace peut être testé indépendamment
  6. IDE Support : potentiel pour auto-complétion avec plugins FreeMarker

Inconvénients et Mitigation

Inconvénient : Légèrement plus verbeux Mitigation : Gain en clarté et maintenabilité compense largement

Inconvénient : Performance légèrement inférieure (appels de méthodes FreeMarker) Mitigation : Différence négligeable en pratique (< 5%)

Inconvénient : Migration de templates existants Mitigation : Support dual + script de migration automatique


📋 Checklist d’Implémentation

Phase 1 : Développement

  • Créer package org.bidji.freemarker.builtin
  • Implémenter FileNamespace.java
  • Implémenter UtilsNamespace.java
  • Implémenter LinesNamespace.java
  • Implémenter JavascriptNamespace.java
  • Implémenter CppNamespace.java
  • Intégrer dans BidjiFreeMarkerModel.java

Phase 2 : Tests

  • Créer tests unitaires pour FileNamespace
  • Créer tests unitaires pour UtilsNamespace
  • Créer tests unitaires pour LinesNamespace
  • Créer templates de test d’intégration
  • Tests de performance (comparaison avant/après)
  • Valider tous les tests passent

Phase 3 : Migration

  • Marquer classes legacy comme @Deprecated
  • Activer support dual dans BidjicTask
  • Créer script de migration de templates
  • Migrer templates de test
  • Documenter nouvelle syntaxe

Phase 4 : Documentation

  • Mettre à jour doc/BidjicDirectives.md
  • Créer exemples avant/après
  • Guide de migration pour utilisateurs
  • Javadoc complète pour tous les namespaces

🚀 Prochaines Étapes

  1. Validation de l’approche : Review de ce document et accord sur l’architecture proposée
  2. Implémentation FileNamespace : Commencer par le namespace le plus utilisé
  3. Tests initiaux : Valider que FreeMarker Custom Directives fonctionnent comme prévu
  4. Feedback : Ajuster l’approche si nécessaire avant d’implémenter tous les namespaces
  5. Rollout progressif : Implémenter namespace par namespace avec tests pour chacun

Conclusion : Cette approche permet de moderniser les builtins tout en gardant une cohérence avec la Phase 3 du refactoring global. Le support dual garantit une migration en douceur sans breaking changes.