git clone https://github.com/kainovaii/obsidian-skeleton.git
cd obsidian-skeleton
./build.bat
Fini les routes déclarées manuellement. Utilise les annotations directement sur tes méthodes.
@GET(value = "/articles", name = "articles.index")
@POST(value = "/articles", name = "articles.store")
@PUT(value = "/articles/:id", name = "articles.update")
@DELETE(value = "/articles/:id", name = "articles.delete")
@PATCH(value = "/articles/:id", name = "articles.patch")
@GET(value = "/articles/:id", name = "articles.show")
private Object show(Request req, Response res) {
String id = req.params(":id");
// ...
}
@GET(value = "/search", name = "search")
private Object search(Request req, Response res) {
String query = req.queryParams("q");
String page = req.queryParams("page");
// ...
}
Intercepte les requêtes avant et après l'exécution des controllers avec des annotations simples.
@Before(LoggingMiddleware.class)
@GET(value = "/dashboard", name = "dashboard")
private Object dashboard(Request req, Response res) {
return render("dashboard.html", Map.of());
}
@Before({CorsMiddleware.class, ApiKeyMiddleware.class, RateLimitMiddleware.class})
@GET(value = "/api/data", name = "api.data")
private Object data(Request req, Response res) {
return "{ \"data\": [] }";
}
@Before(TimingMiddleware.class)
@After(TimingMiddleware.class)
@GET(value = "/profile", name = "profile")
private Object profile(Request req, Response res) {
// TimingMiddleware mesure le temps d'exécution
return render("profile.html", Map.of());
}
@Before({CorsMiddleware.class, ApiKeyMiddleware.class, RateLimitMiddleware.class})
@GET(value = "/api/users", name = "api.users")
private Object users(Request req, Response res) {
// 1. CORS headers ajoutés
// 2. API Key validée (sinon 401)
// 3. Rate limit vérifié (sinon 429)
// 4. Controller exécuté
return "{ \"users\": [] }";
}
public class MaintenanceMiddleware implements Middleware {
@Override
public void handle(Request req, Response res) throws Exception {
boolean isMaintenance = System.getenv("MAINTENANCE_MODE") != null;
if (isMaintenance && !req.pathInfo().equals("/maintenance")) {
res.redirect("/maintenance");
throw new Exception("Maintenance mode");
}
}
}
// Utilisation
@Before(MaintenanceMiddleware.class)
@GET("/dashboard")
private Object dashboard(Request req, Response res) {
return render("dashboard.html", Map.of());
}
# TimingMiddleware
X-Response-Time: 42ms
# RateLimitMiddleware
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
# CorsMiddleware
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-CSRF-TOKEN
INFO: GET /articles - IP: 192.168.1.1 - User-Agent: Mozilla/5.0...
INFO: POST /api/users - IP: 10.0.0.5 - User-Agent: PostmanRuntime/7.26.8
INFO: DELETE /articles/42 - IP: 192.168.1.1 - User-Agent: Mozilla/5.0...
Tous tes controllers doivent hériter de BaseController et porter l'annotation @Controller.
@Controller
public class ArticleController extends BaseController {
@GET(value = "/articles", name = "articles.index")
private Object index(Request req, Response res) {
return render("articles/index.html", Map.of(
"title", "Mes articles"
));
}
@POST(value = "/articles", name = "articles.store")
private Object store(Request req, Response res) {
String title = req.queryParams("title");
Article article = new Article();
article.set("title", title);
article.saveIt();
res.redirect("/articles");
return null;
}
}
@GET(value = "/articles", name = "articles.index")
private Object index(ArticleRepository articleRepo) {
List<Article> articles = DB.withConnection(() ->
articleRepo.findAll().stream().toList()
);
return render("articles/index.html", Map.of(
"articles", articles
));
}
L'injection fonctionne avec n'importe quelle classe annotée @Repository. Tu peux injecter autant de dépendances que nécessaire.
Les models utilisent ActiveJDBC, un ORM léger qui suit le pattern ActiveRecord.
public class Article extends Model {
// Getters
public String getTitle() {
return getString("title");
}
public String getContent() {
return getString("content");
}
public Integer getStatus() {
return getInteger("status");
}
// Setters
public void setTitle(String title) {
set("title", title);
}
public void setContent(String content) {
set("content", content);
}
public void setStatus(Integer status) {
set("status", status);
}
}
// Créer
Article article = new Article();
article.setTitle("Mon titre");
article.setContent("Mon contenu");
article.saveIt();
// Récupérer
Article article = Article.findById(1);
LazyList<Article> articles = Article.findAll();
LazyList<Article> published = Article.where("status = ?", 1);
// Modifier
article.setTitle("Nouveau titre");
article.saveIt();
// Supprimer
article.delete();
public class Article extends Model {
// Un article appartient à un user
public User getAuthor() {
return parent(User.class);
}
// Un article a plusieurs commentaires
public LazyList<Comment> getComments() {
return get(Comment.class, "article_id = ?", getId());
}
}
Un système de migrations fluide inspiré de Laravel pour gérer ta structure de base de données.
public class CreateArticlesTable extends Migration {
@Override
public void up() {
createTable("articles", table -> {
table.id();
table.string("title").notNull();
table.text("content").notNull();
table.integer("status").defaultValue(1);
table.integer("user_id");
table.timestamps();
});
}
@Override
public void down() {
dropTable("articles");
}
}
table.id() // PRIMARY KEY AUTO_INCREMENT
table.string("name") // VARCHAR(255)
table.string("code", 10) // VARCHAR(10)
table.text("content") // TEXT
table.integer("count") // INT
table.bigInteger("big_count") // BIGINT
table.decimal("price", 10, 2) // DECIMAL(10,2)
table.boolean("active") // BOOLEAN
table.date("birth_date") // DATE
table.datetime("event_at") // DATETIME
table.timestamp("logged_at") // TIMESTAMP
table.timestamps() // created_at + updated_at
table.string("email").notNull()
table.integer("count").defaultValue(0)
table.string("code").unique()
table.integer("user_id").foreignKey("users", "id")
Sépare ta logique de données avec le pattern Repository.
@Repository
public class ArticleRepository {
public LazyList<Article> findAll() {
return Article.findAll();
}
public LazyList<Article> findPublished() {
return Article.where("status = ?", 1);
}
public LazyList<Article> findByAuthor(Integer userId) {
return Article.where("user_id = ?", userId);
}
public Article findById(Integer id) {
return Article.findById(id);
}
public void create(String title, String content) {
Article article = new Article();
article.setTitle(title);
article.setContent(content);
article.setStatus(1);
article.saveIt();
}
}
Utilise les repositories pour toute logique complexe de récupération de données. Ça garde tes controllers propres et facilite les tests.
Utilise Pebble pour tes vues. Syntaxe simple et puissante.
{% extends "layout.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<h1>{{ title }}</h1>
{% for article in articles %}
<article>
<h2>{{ article.title }}</h2>
<p>{{ article.content }}</p>
</article>
{% endfor %}
{% endblock %}
return render("articles/index.html", Map.of(
"title", "Mes articles",
"articles", articles
));
{{ article.title | upper }}{{ article.content | truncate(100) }}{{ article.createdAt | date("Y-m-d") }}{{ article.price | number_format }}
// Toutes les opérations DB doivent être wrapped
List<Article> articles = DB.withConnection(() ->
Article.findAll().stream().toList()
);
// Ou pour plusieurs opérations
DB.withConnection(() -> {
Article article = new Article();
article.setTitle("Test");
article.saveIt();
return article;
});
Le système d'authentification est basé sur UserDetailsService, une abstraction qui te laisse libre d'organiser ton modèle User comme tu veux.
@UserDetailsServiceImpl
public class AppUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public AppUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadByUsername(String username) {
return DB.withConnection(() -> {
if (!UserRepository.userExist(username)) return null;
User user = userRepository.findByUsername(username);
return adapt(user);
});
}
@Override
public UserDetails loadById(Object id) {
return DB.withConnection(() -> {
User user = User.findById(id);
return adapt(user);
});
}
private UserDetails adapt(User user) {
if (user == null) return null;
return new UserDetails() {
public Object getId() { return user.getId(); }
public String getUsername() { return user.getUsername(); }
public String getPassword() { return user.getPassword(); }
public String getRole() { return user.getRole(); }
public boolean isEnabled() { return true; }
};
}
}
@Controller
public class LoginController extends BaseController {
@POST("/users/login")
private Object loginBack(Request req, Response res) {
String username = req.queryParams("username");
String password = req.queryParams("password");
Session session = req.session(true);
if (login(username, password, session)) {
res.redirect("/dashboard");
return true;
}
return redirectWithFlash(req, res, "error",
"Incorrect login", "/users/login");
}
@HasRole("DEFAULT")
@GET("/users/logout")
private Object logout(Request req, Response res) {
logout(req.session(true));
return redirectWithFlash(req, res, "success",
"Logged out", "/users/login");
}
}
// Authentification
login(username, password, session) // Authentifie et crée la session
logout(session) // Déconnecte l'utilisateur
// Vérifications
isLogged(req) // Vérifie si connecté
hasRole(req, "ADMIN") // Vérifie un rôle
requireLogin(req, res) // Force la connexion (redirige sinon)
// Récupération
getLoggedUser(req) // Récupère le UserDetails
@HasRole("ADMIN")
@GET("/admin/dashboard")
private Object adminDashboard(Request req, Response res) {
// Accessible uniquement aux ADMIN
return render("admin/dashboard.html", Map.of());
}
@HasRole("DEFAULT")
@GET("/profile")
private Object profile(Request req, Response res) {
// Accessible à tous les utilisateurs connectés
UserDetails user = getLoggedUser(req);
return render("profile.html", Map.of("user", user));
}
// 1. Créer ton interface custom
public interface AppUserDetails extends UserDetails {
String getEmail();
String getIp();
}
// 2. Adapter vers cette interface
private AppUserDetails adapt(User user) {
if (user == null) return null;
return new AppUserDetails() {
public Object getId() { return user.getId(); }
public String getUsername() { return user.getUsername(); }
public String getPassword() { return user.getPassword(); }
public String getRole() { return user.getRole(); }
// Tes champs custom
public String getEmail() { return user.getEmail(); }
public String getIp() { return user.getIp(); }
};
}
// 3. Utiliser dans les controllers
@GET("/profile")
private Object profile(Request req, Response res) {
AppUserDetails user = getLoggedUser(req);
String email = user.getEmail(); // ✅ Disponible !
return render("profile.html", Map.of("user", user));
}
L'annotation @UserDetailsServiceImpl permet au framework de détecter automatiquement ton implémentation. Pas besoin de config manuelle !
Le rôle "DEFAULT" signifie "n'importe quel utilisateur connecté".
Protection automatique contre les attaques Cross-Site Request Forgery.
@CsrfProtect
@POST(value = "/articles", name = "articles.store")
private Object store(Request req, Response res) {
// Token validé automatiquement
// Si invalide → 403 Forbidden
}
<form method="POST" action="/articles">
{{ csrf_field() | raw }}
<input type="text" name="title">
<button type="submit">Create</button>
</form>
const csrfToken = '{{ csrf_token() }}';
fetch('/articles', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({ title: 'My article' })
});
@POST("/login")
private Object login(Request req, Response res) {
if (isLogged(req)) {
regenerateCsrfToken(req);
res.redirect("/dashboard");
}
return null;
}
Affiche des notifications temporaires après des actions utilisateur. Aucune dépendance externe requise.
@Post("/login")
public Object login(Request req, Response res)
{
String username = req.queryParams("username");
String password = req.queryParams("password");
if (login(username, password, req.session())) {
return redirectWithFlash(req, res, "success", "Welcome back!", "/dashboard");
}
return redirectWithFlash(req, res, "error", "Invalid credentials", "/login");
}
// Success (vert/orange)
redirectWithFlash(req, res, "success", "Operation completed!", "/");
// Error (rouge)
redirectWithFlash(req, res, "error", "Something went wrong", "/");
// Info (bleu)
redirectWithFlash(req, res, "info", "Check your email", "/");
// Warning (jaune/orange)
redirectWithFlash(req, res, "warning", "Changes require restart", "/");
<!DOCTYPE html>
<html>
<body>
<nav>...</nav>
{% block content %}{% endblock %}
{{ flash() }}
</body>
</html>
// Dans Main.java
public static void configureFlash()
{
// Changer la durée (en ms)
FlashConfig.setDuration(5000);
// Changer la position
FlashConfig.setPosition("top-right");
// Options: top-right, top-left, bottom-right,
// bottom-left, top-center, bottom-center
// CSS personnalisé
FlashConfig.setCustomCSS("""
.flash-notification {
border-radius: 1rem;
font-size: 0.875rem;
}
.flash-success {
background: linear-gradient(to right, #10b981, #059669);
}
""");
}