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
- 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
- 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
- PageBuiltinTest.java (31 tests) - id, title, original_title - Gestion du contenu - addLine, hasLines - Unicode et caractères spéciaux
- 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
- base_name() retire TOUT après le premier point “My.File.With.Dots.txt” → “My” // Pas “My.File.With.Dots” !
- Duplication entre FileBuiltin et BidjiBuiltIn - BidjiBuiltIn a des méthodes @Deprecated qui dupliquent FileBuiltin
- 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 :
- Valider que les Custom Directives FreeMarker reproduisent le même comportement
- Tests de non-régression pendant le refactoring
- 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
bjiocréé 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.shpour 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
- Analyse de l’Existant
- Proposition d’Architecture
- Exemples de Code
- Plan d’Implémentation
- Migration Progressive
- 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)
-
Créer le package
org.bidji.freemarker.builtin -
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)
-
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 :
- Release N : Support dual - anciennes et nouvelles syntaxes fonctionnent
- Release N+1 : Warnings de dépréciation dans les logs
- Release N+2 : Documentation updated - nouvelle syntaxe recommandée
- 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)avecfileen 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
- Cohérence avec Phase 3 : même pattern pour entity/property et file/utils
- Validation native : FreeMarker valide les arguments automatiquement
- Meilleure documentation : namespace clair
bidji.file.*,bidji.utils.* - Extensibilité : facile d’ajouter de nouvelles méthodes
- Testabilité : chaque namespace peut être testé indépendamment
- 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
- Validation de l’approche : Review de ce document et accord sur l’architecture proposée
- Implémentation FileNamespace : Commencer par le namespace le plus utilisé
- Tests initiaux : Valider que FreeMarker Custom Directives fonctionnent comme prévu
- Feedback : Ajuster l’approche si nécessaire avant d’implémenter tous les namespaces
- 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.