merge: preserve source-verified tinystruct documentation 3231

This commit is contained in:
haelyra
2026-10-09 17:04:47 -04:00
5 changed files with 702 additions and 56 deletions
+332 -36
View File
@@ -1,28 +1,114 @@
---
name: tinystruct-patterns
description: tinystructフレームワークでアプリケーションモジュールまたはマイクロサービスを開発する際に使用。ルーティング、コンテキスト管理、BuilderによるJSON処理、CLI/HTTPデュアルモードのパターンをカバー。
origin: ECC
description: tinystruct Java フレームワークで開発する際の専門ガイダンス。tinystruct コードベース、または tinystruct 上に構築されたあらゆるプロジェクトで作業する際に使用します — プロジェクトに存在しない場合の bin/dispatcher・bin/dispatcher.cmd 起動スクリプトの生成、Application クラスの作成、@Action によるルート定義、ユニットテスト、ActionRegistry、HTTP/CLI デュアルモード対応、組み込み HTTP サーバー、イベントシステム、Builder/Builders による JSON 処理、AbstractData によるデータベース永続化、POJO 生成、Server-Sent Events (SSE)、ファイルアップロード、アウトバウンド HTTP 通信を含みます。
metadata:
origin: ECC
upstream: tinystruct==1.7.34
---
# tinystruct 開発パターン
**tinystruct** Java フレームワークを使用してモジュールをビルドするためのアーキテクチャと実装パターン。CLIとHTTPが等しく扱われる軽量なシステムです。
**tinystruct** Java フレームワークを使用してモジュールをビルドするためのアーキテクチャと実装パターン。CLI と HTTP を等しく扱う軽量・高性能なフレームワークで、`main()` メソッドを必要とせず、最小限の設定で動作します。
## コア原則
**CLI と HTTP は等しい市民(equal citizens)です。** `@Action` でアノテーションされたすべてのメソッドは、理想的には変更なしでターミナルとWebブラウザの両方から実行可能であるべきです。この「デュアルモード」対応能力が tinystruct のコア設計思想です。
## 主要な開発ツール:`bin/dispatcher`
**`bin/dispatcher` は tinystruct アプリケーションを開発・実行・テスト・デバッグするためのデフォルトかつ最優先のツールです — IDE の実行構成、手書きの `main()`、curl、ブラウザよりも先にこれを使ってください。** すべての `@Action` は設計上デュアルモードであるため、`bin/dispatcher` を使うことでサーバーやブラウザを必要とせず、ターミナルから直接ルーティング・引数バインディング・ビジネスロジックを、最速のフィードバックループで検証できます。
```bash
# @Action を直接実行 - 新しいアクションが動作するか確認する最速の方法
# (クラスをインポートする必要があります。--import または application.properties の default.import.applications を参照してください)
bin/dispatcher greet/James --import com.example.MyService
bin/dispatcher echo --words "Praise the Lord"
# Web向けの対応部分を検証する必要があるときに HTTP サーバーを起動
bin/dispatcher start --import org.tinystruct.system.HttpServer
# 今回の実行のために追加の Application/MCP クラスをインポート
# (クラスごとに --import を1つ。カンマ区切りのリストは ClassNotFoundException になる)
bin/dispatcher start --import org.tinystruct.system.HttpServer --import com.example.MyService
# 利用可能な内容を確認
bin/dispatcher --help
bin/dispatcher --version
```
このワークフローをデフォルトにしてください:`@Action` を実装したら、`bin/dispatcher <action>` ですぐに実行して CLI モードで正しく動作することを確認し、Web向けのパス(例:`mode = Mode.HTTP_POST`、セッション、ファイルアップロードなど)を検証する必要があるときだけ HTTP サーバー(これも `bin/dispatcher start ...` で)を起動します。アプリのエントリポイントとして `main(String[] args)` を決してハードコードしないでください — `bin/dispatcher`(Windows では `bin/dispatcher.cmd`)がすべてのモジュールの唯一のエントリポイントです。
### `bin/dispatcher` と `bin/dispatcher.cmd` の生成
すべての tinystruct プロジェクトは `bin/` に起動スクリプトを必要とします。**フレームワークがこれを生成します — 手で書いたり、他のプロジェクトからコピーしたり、`main()` で代用したりしないでください。** `ApplicationManager.init()` は、起動スクリプトが存在しない場合、tinystruct jar 内のテンプレートからそのjarのバージョンを埋め込んだ、実行中のOS用のスクリプトを作業ディレクトリ配下の `bin/` に書き出します。改行コードと実行権限はすでに正しく設定されています。
**1. プロジェクトがビルド対象とする tinystruct のバージョンを確認する**:`pom.xml` の `<tinystruct.version>` プロパティ、または `org.tinystruct:tinystruct` 依存関係です。jar は `~/.m2/repository/org/tinystruct/tinystruct/<version>/`(なければ `mvn dependency:resolve` を実行)か、プロジェクトの `lib/` に存在している必要があります。
**2. プロジェクトルートから、フレームワークを一度実行します:**
```bash
java -cp ~/.m2/repository/org/tinystruct/tinystruct/<version>/tinystruct-<version>.jar \
org.tinystruct.system.Dispatcher --version
```
`<version>` をステップ1で確認したバージョンに置き換えてください。Windows では `%USERPROFILE%\.m2\repository\org\tinystruct\tinystruct\<version>\tinystruct-<version>.jar` と同じクラスを使用します。これにより**実行したOS用のスクリプトだけ**が、まだ存在しない場合にのみ作成されます:
| 実行環境 | 作成されるもの |
|---|---|
| Linux, macOS | `bin/dispatcher`(実行可能、LF) |
| Windows | `bin\dispatcher.cmd`(CRLF) |
**3. 検証する。** プロジェクトルートから `bin/dispatcher --version`(Windows では `bin\dispatcher.cmd --version`)を実行します。`Dispatcher (cli) (built on tinystruct-<version>)` と表示され、スクリプト内の `VERSION` は jar のバージョンと一致します。続けて `bin/dispatcher --help` を実行すると、コマンド一覧とインポートされたすべてのアクションが表示されます。作成したファイルをユーザーに見せてください。
**4. 両方のスクリプトを取得するには**、各OSでステップ2を実行するか、もう一方のOSでチームメイトやCIに実行させてその結果をコミットしてもらいます。どちらのOSでチェックアウトしても動作するよう、両方をコミットしてください。`-Dos.name=…` で別のOSを偽装しようとしないでください:Windows では `setPosixFilePermissions` の `UnsupportedOperationException` で失敗し、空の `bin/dispatcher` が残ります。チェックアウト時に改行コードが壊れないよう、以下の `.gitattributes` を推奨します:
```
bin/dispatcher text eol=lf
bin/dispatcher.cmd text eol=crlf
```
**tinystruct のアップグレード:** `pom.xml` のバージョンを変更し、古いスクリプトを削除して、新しい jar でステップ2を繰り返します。あるいは `bin/dispatcher update` が Maven Central で最新リリースを確認し、プロジェクトの依存関係をアップグレードしてスクリプトを強制的に再生成します。これにはネットワークが必要で、`pom.xml` を編集し、選択したバージョンではなく*最新*バージョンに移行します。
**これは暗黙的にも発生します。** `ApplicationManager.init()` に到達するコード(dispatcher 自体、またはユニットテスト内の `ApplicationManager.install(app, config)`)は、スクリプトが存在しない場合、*カレントディレクトリ*にそれを作成します。ユニットテストが `bin/dispatcher.cmd` を各モジュールのディレクトリに散らかしてしまう場合は、ビルドディレクトリから実行してください:surefire の設定で `<workingDirectory>${project.build.directory}</workingDirectory>` を設定します。
**スクリプトの動作内容**(ほとんどの問題を説明します):
- 必ず**プロジェクトルートから**実行してください:シェルスクリプトはカレントディレクトリをルートとみなし、`.cmd` は `bin/` の親ディレクトリをルートとみなします。
- クラスパスは `target/classes`、`lib/*.jar`、`WEB-INF/lib/*`、`WEB-INF/classes`、そして tinystruct の jar(`lib/` になければ `~/.m2`)で構成され、その後 `org.tinystruct.system.Dispatcher` を引数付きで実行します。
- 初回実行時、`mvnw` / `mvnw.cmd` が存在しない場合は、プロジェクトルートに Maven Wrapper を展開します(Unix では `unzip`、Windows では PowerShell)。これが起こることをユーザーに伝えてください。ファイルはコミットしても、無視してもかまいません。
- `-D…` や `-X…` 引数を JVM オプションとして扱うのはシェルスクリプトのみです。
**注意点:**
- **Windows では `bin\dispatcher.cmd` を使ってください。** シェルスクリプトはクラスパスを `:` で結合するため、Git Bash/MSYS 上では `ClassNotFoundException: org.tinystruct.system.Dispatcher` で失敗します。WSL か `.cmd` を使用してください。
- **クラスパスに含まれるのは tinystruct の jar のみです。** フレームワークはデフォルトで fat jar を提供しなくなったため、他のモジュール、JDBCドライバ、ライブラリ(jjwt、lettuce など)は `lib/` に置く必要があります:`mvn dependency:copy-dependencies -DoutputDirectory=lib`(`lib/` は git-ignore してください)。マルチモジュールビルドでは、起動スクリプトを `target/classes` と `lib/` を持つディレクトリに置き、兄弟モジュールの jar をその `lib/` にコピーしてください。兄弟モジュールがクラスパスに含まれないディレクトリから実行すると `NoClassDefFoundError` になります。
- **アプリケーションは `--import` または設定でロードしてください。** クラスごとに `--import` を1つ(`--import a.A,b.B` は失敗します。`--import a.A --import b.B` と書いてください)、または `application.properties` に一度だけ列挙します:`default.import.applications=a.A;b.B`(`;` 区切り)。
- **`.cmd` には `JAVA_HOME` の設定が必要です**。設定されていないとエラーで停止します。
- **`bin/` は `target/classes` と `lib/` を持つディレクトリの直下に置いてください。** `.cmd` は `bin/` の親ディレクトリをルートとみなすため、`bin/` が誤った場所にネストしていると、間違ったクラスパスが `Dispatcher` の前に付与されます。
## 使用するタイミング
### 使用する場面
- `bin/dispatcher` を通じて `@Action` を実行・テスト・デバッグするとき — これは HTTP や IDE の実行構成に頼る前の、tinystruct アプリを扱うデフォルトの方法です。
- `bin/dispatcher` / `bin/dispatcher.cmd` が存在しないプロジェクトをセットアップするとき — フレームワークに生成させてください(「`bin/dispatcher` と `bin/dispatcher.cmd` の生成」を参照)。
- `AbstractApplication` を拡張して新しい `Application` モジュールを作成するとき。
- `@Action` を使用してルートとコマンドラインアクションを定義するとき。
- `Context` を通じてリクエストごとの状態を処理するとき。
- ネイティブの `Builder` コンポーネントを使用してJSONシリアライゼーションを行うとき。
- `application.properties` でデータベース接続またはシステム設定を構成するとき。
- `ApplicationManager.init()` を通じて標準的な `bin/dispatcher` エントリポイントを生成または再生成するとき。
- ルーティング競合(Action)またはCLI引数解析のデバッグを行うとき。
- ネイティブの `Builder` と `Builders` コンポーネントを使用してJSONシリアライゼーションを行うとき。
- `AbstractData` POJO を使用してデータベース永続化を行うとき。
- `generate` コマンドを使用してデータベーステーブルから POJO を生成するとき(XML マッピングファイルまたはアノテーション `--mapping annotation`)。
- リアルタイムプッシュのために Server-Sent Events (SSE) を実装するとき。
- multipart データによるファイルアップロードを処理するとき。
- `URLRequest` と `HTTPHandler` を使用してアウトバウンドHTTPリクエストを行うとき。
- `application.properties` でデータベース接続やシステム設定を構成するとき。
- ルーティングの競合(Action)や CLI 引数解析をデバッグするとき。
## 動作の仕組み
tinystruct フレームワークは、`@Action` でアノテーションされたメソッドをターミナルとWeb環境の両方でルーティング可能なエンドポイントとして扱います。アプリケーションは `AbstractApplication` を拡張することで作成され、`init()` などのコアライフサイクルフックとリクエスト `Context` へのアクセスが提供されます。
ルーティングは `ActionRegistry` によって処理され、パスセグメントをメソッド引数に自動的にマッピングして依存関係を注入します。データのみのサービスでは、ゼロ依存のフットプリントを維持するために、JSONシリアライゼーションにネイティブの `Builder` コンポーネントを使用すべきです。フレームワークには `ApplicationManager` のユーティリティも含まれており、`bin/dispatcher` スクリプトを生成することでプロジェクトの実行環境をブートストラップします。
ルーティングは `ActionRegistry` によって処理され、パスセグメントをメソッド引数に自動的にマッピングして依存関係を注入します。データのみのサービスでは、ゼロ依存のフットプリントを維持するために、JSONシリアライゼーションにネイティブの `Builder` と `Builders` コンポーネントを使用すべきです。データベース層は `@Table`/`@Column` アノテーションまたは XML マッピングファイルでテーブルにマッピングされた `AbstractData` POJO を使用し、外部 ORM ライブラリなしで CRUD 操作を行います。不足しているテーブルは `database.autocreate=true` で自動的に作成できます。
## 例
@@ -40,58 +126,247 @@ public class MyService extends AbstractApplication {
public String greet() {
return "Hello from tinystruct!";
}
}
```
### パラメータ付きルーティング(getUser)
```java
// Web: /api/user/123 または CLI: "bin/dispatcher api/user/123" を処理
@Action("api/user/(\\d+)")
public String getUser(int userId) {
return "User ID: " + userId;
// パスパラメータ: GET /?q=greet/James または bin/dispatcher greet/James
@Action("greet")
public String greet(String name) {
return "Hello, " + name + "!";
}
}
```
### HTTPモード分岐(login)
```java
@Action(value = "login", mode = Mode.HTTP_POST)
public boolean doLogin() {
// ログイン処理
return true;
public String doLogin(Request<?, ?> request) throws ApplicationException {
request.getSession().setAttribute("userId", "42");
return "Logged in";
}
```
### ネイティブJSONデータ処理(getData)
### ネイティブJSONデータ処理(Builder + Builders)
```java
import org.tinystruct.data.component.Builder;
import org.tinystruct.data.component.Builders;
@Action("api/data")
public Builder getData() throws ApplicationException {
Builder builder = new Builder();
builder.put("status", "success");
Builder nested = new Builder();
nested.put("id", 1);
nested.put("name", "James");
builder.put("data", nested);
return builder;
public String getData() throws ApplicationException {
Builders dataList = new Builders();
Builder item = new Builder();
item.put("id", 1);
item.put("name", "James");
dataList.add(item);
Builder response = new Builder();
response.put("status", "success");
response.put("data", dataList);
return response.toString(); // {"status":"success","data":[{"id":1,"name":"James"}]}
}
```
### SSE(Server-Sent Events)
```java
import org.tinystruct.http.SSEPushManager;
@Action("sse/connect")
public String connect() {
return "{\"type\":\"connect\",\"message\":\"Connected to SSE\"}";
}
// 特定のクライアントへプッシュ
String sessionId = getContext().getId();
Builder msg = new Builder();
msg.put("text", "Hello, user!");
SSEPushManager.getInstance().push(sessionId, msg);
// 全員へブロードキャスト
// 全員へブロードキャスト
SSEPushManager.getInstance().broadcast(msg);
```
### ファイルアップロード
```java
import org.tinystruct.data.FileEntity;
@Action(value = "upload", mode = Mode.HTTP_POST)
public String upload(Request<?, ?> request) throws ApplicationException {
List<FileEntity> files = request.getAttachments();
if (files != null) {
for (FileEntity file : files) {
System.out.println("Uploaded: " + file.getFilename());
}
}
return "Upload OK";
}
```
### データベースメタデータ操作
`DatabaseOperator` は、接続レベルのカタログ、スキーマ、メタデータへの直接アクセスを提供します:
```java
import org.tinystruct.data.DatabaseOperator;
DatabaseOperator operator = new DatabaseOperator();
try {
String catalog = operator.getCatalog();
String schema = operator.getSchema();
java.sql.DatabaseMetaData metaData = operator.getMetaData();
System.out.println("Using catalog: " + catalog + ", schema: " + schema);
} finally {
operator.close();
}
```
## MCP サーバーとツールの統合
tinystruct は SDK バージョン **`1.7.0`** から Model Context Protocol (MCP) のネイティブサポートを提供しています。
MCP API(例:`org.tinystruct.mcp.MCPTool`、`org.tinystruct.mcp.MCPServer`、`org.tinystruct.mcp.MCPException`)はコアの依存関係に直接含まれています:
```xml
<dependency>
<groupId>org.tinystruct</groupId>
<artifactId>tinystruct</artifactId>
<version>1.7.34</version>
</dependency>
```
> **セキュリティ警告(プロンプトインジェクション):**
> MCPクライアントまたはモデルホストがツールの戻り値をモデルのコンテキストに渡す場合、その戻り値は信頼できないデータです。呼び出し元から渡された引数だけでなく、データベース、ファイル、ネットワークなどから取得した**すべてのツール出力を検証し、モデルの指示と分離する必要があります**。戻り値内の命令を信頼できる指示として扱うと、攻撃者がモデルの挙動を上書きするプロンプトインジェクションを許してしまいます。
**MCP ツールを作成するには:**
1. `org.tinystruct.mcp.MCPTool` を拡張する。
2. `@Action` で操作にアノテーションを付け、`arguments` 配列内で `@Argument` を使ってパラメータを宣言する。
3. パラメータは `@Argument` のキーに対応する明示的なメソッド引数として受け取る(ツールの引数に `getContext().getAttribute(...)` を使用**しないでください**)。
```java
import org.tinystruct.mcp.MCPTool;
import org.tinystruct.mcp.MCPException;
import org.tinystruct.system.annotation.Action;
import org.tinystruct.system.annotation.Argument;
public class MyCustomTool extends MCPTool {
public MyCustomTool() {
super("custom", "A custom tool for demonstrating MCP");
}
@Action(
value = "custom/hello",
description = "Say hello to someone",
arguments = {
@Argument(key = "name", description = "The name to greet", type = "string", optional = false)
}
)
public String hello(String name) throws MCPException {
// SECURITY: モデルに返す前にツール入力を検証・サニタイズし、
// プロンプトインジェクションの脆弱性を防止する。
if (name == null || name.length() > 50 || !name.matches("^[a-zA-Z0-9 ]+$")) {
throw new MCPException("Invalid name provided");
}
return "Hello, " + name + "!";
}
}
```
**MCP サーバーをデプロイするには:**
1. `org.tinystruct.mcp.MCPServer` を拡張する。
2. `init()` をオーバーライドし、`this.registerTool()` を使ってツールを登録する。フレームワークが `@Action` メソッドを自動的にスキャンしてマッピングします。
```java
import org.tinystruct.mcp.MCPServer;
public class MyMCPServer extends MCPServer {
@Override
public void init() {
super.init();
this.registerTool(new MyCustomTool());
}
@Override
public String version() {
return "1.0.0";
}
}
```
dispatcher 経由でサーバーを実行します:
```bash
bin/dispatcher start --import org.tinystruct.system.HttpServer --import com.example.MyMCPServer
```
### オーバーロードされたツールメソッド
tinystruct は `MCPTool` 内で同一ツール名に対するオーバーロードメソッド(同じツール名を共有しつつ異なるパラメータシグネチャを受け取る)をサポートしています。
- **スキーママージ**:同じ名前を持つすべてのオーバーロードの入力スキーマは、動的に統合されたひとつの JSON スキーマにマージされます。プロパティは和集合として扱われ、必須プロパティは積集合として扱われます(*すべての*オーバーロードで必須とされているフィールドのみが必須として残ります)。
- **実行ルーティング**:ツールを実行する際、サーバーは利用可能なオーバーロードメソッドを順に試行します。各メソッドのスキーマ検証が最初に行われ、提供された引数に対して最初に検証を通過したオーバーロードが実行されます。どのシグネチャも一致・成功しない場合、フレームワークは適切な例外をスローします。
## 設定
設定は `src/main/resources/application.properties` で管理されます。
## テストパターン
```properties
# データベース
driver=org.h2.Driver
database.url=jdbc:h2:~/mydb
database.user=sa
database.password=
# 任意:初回利用時にクラスのマッピングから不足しているテーブルを作成する(デフォルトはオフ)
# database.autocreate=true
JUnit 5 を使用して、アクションが `ActionRegistry` に登録されていることを検証することでアクションをテストします。
# サーバー
default.home.page=hello
server.port=8080
default.server.open_browser=true
# ロケール
default.language=en_US
# セッション(クラスタ環境向けの Redis)
# default.session.repository=org.tinystruct.http.RedisSessionRepository
# redis.host=127.0.0.1
# redis.port=6379
# プログラムによるロギング設定
logging.enabled=true
logging.level=INFO
org.tinystruct.level=FINE
```
アプリケーション内で設定値にアクセスする:
```java
String port = this.getConfiguration("server.port");
```
## ロギングと診断
フレームワークには `java.util.logging`(JUL)を包む形のプログラム的なラッパーが含まれており、標準で見やすいコンソール出力と高度な呼び出し元トレースを提供します。
- **ANSI コンソールカラー**:コンソール出力はログレベルに応じて色分けされます(SEVERE は赤、WARNING は黄、INFO は緑、CONFIG はシアン、FINE/デバッグログはグレー)。
- **正確な呼び出し元トレース**:Java の `StackWalker` API を使用して実行時のコールスタックをトレースします。ログを発生させた呼び出し元の正確なクラス、メソッド、ファイル名、行番号を特定してログに記録します(内部のユーティリティ層やロギング層は経由しません)。
- **パッケージ/ロガーレベルの上書き**:`application.properties` にパッケージ単位の上書きを直接設定できます(例:`org.tinystruct.level=FINE`)。
## レッドフラグとアンチパターン
| 症状 | 正しいパターン |
|---|---|
| `com.google.gson` または `com.fasterxml.jackson` のインポート | `org.tinystruct.data.component.Builder` を使用する。 |
| `.view` ファイルの `FileNotFoundException` | APIのみのアプリでは `init()` 内で `setTemplateRequired(false)` を呼び出す。 |
| `com.google.gson` や `com.fasterxml.jackson` のインポート | `org.tinystruct.data.component.Builder` / `Builders` を使用する。 |
| JSON配列に `List<Builder>` を使用する | ジェネリックの型消去の問題を避けるため `Builders` を使用する。 |
| `ApplicationRuntimeException: template not found` | API専用アプリでは `init()` 内で `setTemplateRequired(false)` を呼び出す。 |
| `private` メソッドへの `@Action` アノテーション | アクションはフレームワークに登録されるために `public` である必要がある。 |
| アプリ内での `main(String[] args)` のハードコーディング | すべてのモジュールのエントリポイントとして `bin/dispatcher` を使用する。 |
| アプリ内で `main(String[] args)` をハードコードする、または curl/ブラウザ/IDE の実行構成だけでテストする | すべてのモジュールのエントリポイント兼デフォルトの開発/テストツールとして `bin/dispatcher` を使用する。 |
| `bin/dispatcher` が存在しない、または手書き・コピーされた起動スクリプトになっている | フレームワークに生成させる:プロジェクトルートから `org.tinystruct.system.Dispatcher --version` を一度実行する(現在のOS用のスクリプトが作成される)。 |
| Windows で `bin/dispatcher` が `ClassNotFoundException: org.tinystruct.system.Dispatcher` で失敗する | シェルスクリプトは Git Bash/MSYS 上では実行できない。`bin\dispatcher.cmd` または WSL を使用する。 |
| `--import a.A,b.B` で `ClassNotFoundException` が発生する | クラスごとに `--import` を1つ:`--import a.A --import b.B`。 |
| シェルスクリプトの起動ファイルが `\r: command not found` で失敗する | ファイルが CRLF 改行になっている。`bin/dispatcher` を LF に変換する。 |
| 手動での `ActionRegistry` 登録 | 自動検出のために `@Action` アノテーションを優先する。 |
| 実行時にアクションが見つからない | クラスが `--import` でインポートされているか、`application.properties` に列挙されているか確認する。 |
| CLI引数が認識されない | `--key value` の形で渡し、`getContext().getAttribute("--key")` でアクセスする。 |
| 同じパスの2つのメソッドで、意図しない方が呼ばれる | 明示的な `mode`(例:`HTTP_GET` と `HTTP_POST`)を設定して区別する。 |
## ベストプラクティス
1. **`bin/dispatcher` を最優先に**:何よりもまず `bin/dispatcher` に対して開発・検証を行ってください — CLI からアクションを実行して挙動を確認し、その後 HTTP/モードに関する関心事を積み重ねます。これが最速のインナーループであり、フレームワークが実際にルーティングと引数バインディングを行う方法と確実に一致する唯一のツールです。
2. **粒度の細かいアプリケーション**:巨大な単一クラスではなく、ロジックを小さく焦点を絞ったアプリケーションに分割する。
3. **`init()` でのセットアップ**:コンストラクタではなく `init()` をセットアップ(設定、DB など)に活用する。`setAction()` を呼び出さず、`@Action` アノテーションを使用する。
4. **モードへの意識**:`@Action` の `Mode` パラメータを使用して、機密性の高い操作を `CLI` のみ、または特定の HTTP メソッドに制限する。
5. **パラメータより Context を優先**:任意の CLI フラグには、メソッドシグネチャにパラメータを追加するのではなく `getContext().getAttribute("--flag")` を使用する。
6. **非同期イベント**:イベントによってトリガーされる重い処理には、イベントハンドラ内で `CompletableFuture.runAsync()` を使用する。
## テクニカルリファレンス
@@ -99,6 +374,27 @@ JUnit 5 を使用して、アクションが `ActionRegistry` に登録されて
- [アーキテクチャと設定](references/architecture.md) — 抽象化、パッケージマップ、プロパティ
- [ルーティングと@Action](references/routing.md) — アノテーションの詳細、モード、パラメータ
- [データ処理](references/data-handling.md) — JSONのためのネイティブ `Builder` の使用
- [システムと使用方法](references/system-usage.md) — Context、セッション、イベント、CLI使用方法
- [テストパターン](references/testing.md) — JUnit 5 統合と ActionRegistry テスト
- [データ処理](references/data-handling.md) — Builder、Builders、JSONのシリアライズ・パース
- [データベース永続化](references/database.md) — AbstractData POJO、CRUD、アノテーションとXMLマッピング、POJO生成、テーブルの自動作成
- [システムと使用方法](references/system-usage.md) — Context、セッション、SSE、ファイルアップロード、イベント、ネットワーキング
- [テストパターン](references/testing.md) — JUnit 5 によるユニットテストとHTTP統合テスト
## 参照元ソースファイル(内部用)
- `src/main/java/org/tinystruct/AbstractApplication.java` — ライフサイクルフックを持つコアベースクラス
- `src/main/java/org/tinystruct/system/annotation/Action.java` — アノテーションとモード
- `src/main/java/org/tinystruct/application/ActionRegistry.java` — ルーティングエンジン
- `src/main/java/org/tinystruct/data/component/Builder.java` — JSONオブジェクトのシリアライザ
- `src/main/java/org/tinystruct/data/component/Builders.java` — JSON配列のシリアライザ
- `src/main/java/org/tinystruct/data/component/AbstractData.java` — CRUDを持つ基底POJOクラス
- `src/main/java/org/tinystruct/data/Mapping.java` — マッピングメタデータ(アノテーションまたはXML)、クラスごとにキャッシュ
- `src/main/java/org/tinystruct/data/annotation/Table.java` — `@Table`(`@Id` と `@Column` も同梱)
- `src/main/java/org/tinystruct/data/tools/TableCreator.java` — 不足しているテーブルを作成する(`database.autocreate`)
- `src/main/java/org/tinystruct/data/tools/MySQLGenerator.java` — POJOジェネレータのリファレンス(`MappingMode` でXMLかアノテーションかを選択)
- `src/main/java/org/tinystruct/data/component/FieldType.java` — SQLからJavaへの型マッピング
- `src/main/java/org/tinystruct/data/component/Condition.java` — 流暢なSQLクエリビルダー
- `src/main/java/org/tinystruct/http/SSEPushManager.java` — SSE接続管理
- `src/main/java/org/tinystruct/system/logging/LogFormatter.java` — ANSIコンソールカラーとStackWalkerベースの呼び出し元トレースを備えたカスタムログフォーマッタ
- `src/main/java/org/tinystruct/system/logging/LoggerConfigurer.java` — アプリケーションプロパティからのプログラムによるロギング構成
- `src/test/java/org/tinystruct/application/ActionRegistryTest.java` — レジストリのテスト例
- `src/test/java/org/tinystruct/system/HttpServerHttpModeTest.java` — HTTP統合テストパターン
@@ -0,0 +1,164 @@
# tinystruct データベース永続化
## 使用する場面
データベース操作にはフレームワーク組み込みのORM的なデータ層を使用します。これは `AbstractData` を拡張したPOJOを、アノテーションまたはXMLマッピングファイルでテーブルにマッピングし、JPA/Hibernateの軽量な代替として機能します。
## 動作の仕組み
### アーキテクチャ
各テーブルは以下によって表されます:
1. **Java POJO**:`AbstractData` を拡張し、getter/setter と `setData(Row)` を提供する。
2. **マッピング**、いずれか一方:
- **アノテーション**:クラスに `@Table`、フィールドに `@Column`([アノテーションマッピング](#アノテーションマッピング)を参照)。
- **マッピングXML**:リソースに置かれた `ClassName.map.xml` で、Javaフィールドとデータベースカラムを結びつける。
マッピングはクラスごとに一度だけ読み込まれてキャッシュされるため、インスタンスの生成で再読み込みや再解析は行われません。クラスに `@Table` が存在する場合、同名の `.map.xml` より優先されます。
#### 主要な基底クラス:`AbstractData`
CRUDメソッドを提供します:
- `append()` / `appendAndGetId()`
- `update()`
- `delete()`
- `findAll()` / `findOneById()` / `findOneByKey(key, value)`
- `findWith(where, params)`
- `find(SQL, params)`
### POJOの生成(CLI)
ライブのデータベーステーブルを内省してPOJOとそのマッピングを生成します。デフォルトでは `.map.xml` ファイルが、`annotation` オプションを指定するとPOJO自体にアノテーションが付与されます。
#### 設定
`application.properties`:
```properties
driver=com.mysql.cj.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/mydb
database.user=root
database.password=secret
```
#### コマンド
```bash
# インタラクティブモード
bin/dispatcher generate
# テーブルを指定
bin/dispatcher generate --tables users
# .map.xml ではなくPOJOにアノテーションを付与
bin/dispatcher generate --tables users --mapping annotation
```
`--mapping` は `xml`(デフォルト)または `annotation` を受け付けます。アノテーションモードでは `.map.xml` は書き出されず、リソースディレクトリも作成されません。不明な値を指定するとプロンプトの前にエラーで拒否されます。
## 例
### CRUD操作
```java
// CREATE
User user = new User();
user.setUsername("james");
user.append();
// READ
User user = new User();
user.setId(42);
user.findOneById();
// UPDATE
user.setEmail("new@example.com");
user.update();
// DELETE
user.delete();
```
### 条件を使ったクエリ
```java
User user = new User();
Table results = user.findWith("username LIKE ?", new Object[]{"%jam%"});
// 流暢なConditionビルダー
Condition condition = new Condition();
condition.setRequestFields("id,username");
Table filtered = user.find(
condition.select("`users`").and("email LIKE ?").orderBy("id DESC"),
new Object[]{"%@example.com"}
);
```
### マッピングXMLの構造
`User.map.xml`:
```xml
<mapping>
<class name="User" table="users">
<id name="Id" column="id" autoIncrement="true" autoGenerated="false" length="11" type="int"/>
<property name="username" column="username" length="50" type="varchar"/>
<property name="email" column="email" length="100" type="varchar"/>
</class>
</mapping>
```
### アノテーションマッピング
アノテーションを使った同じ `User`(`generate --mapping annotation` が生成するもの):
```java
import org.tinystruct.data.annotation.Column;
import org.tinystruct.data.annotation.Id;
import org.tinystruct.data.annotation.Table;
import org.tinystruct.data.component.AbstractData;
import java.io.Serializable;
@Table(name = "users",
id = @Id(name = "Id", column = "id", type = "int", length = 11, autoIncrement = true))
public class User extends AbstractData implements Serializable {
@Column(name = "username", type = "varchar", length = 50)
private String username;
@Column(name = "email", type = "varchar", length = 100)
private String email;
// getter、setter(setFieldAsXxx を使用)、および setData(Row) を通常通り実装
}
```
| アノテーション | 対象 | 属性 |
|---|---|---|
| `@Table` | クラス | `name`(必須)、`schema`(デフォルト:なし)、`id`(デフォルト:識別子なし) |
| `@Id` | `@Table(id = ...)` 内 | `name`(デフォルト `Id`)、`column`、`type`、`length`、`autoIncrement`、`autoGenerated`。`column` が空の場合、テーブルに識別子がないことを意味する。 |
| `@Column` | フィールド | `type`(必須)、`name`(DBカラム名。デフォルトはJavaフィールド名)、`length` |
- 識別子は `AbstractData` から継承されるため、独自のフィールドに `@Id` でアノテーションするのではなく `@Table` 内で記述する。
- `type` はXMLの `type` 属性と同じ名前(`FieldType` の名前、大文字小文字を無視してマッチング)を取る:`INT`、`BIGINT`、`VARCHAR`、`TEXT`、`DATE`、`TIMESTAMP` など。
- `@Column` が付与されたフィールドのみマッピングされる。`@Table` は継承され、スーパークラスのアノテーション付きフィールドもマッピングされる。
- 同じプロパティを二重にマッピング(例:スーパークラスのアノテーション付きフィールドを隠すサブクラスフィールド)すると、そのクラスが最初に使われる際にエラーになる。
- `autoGenerated = true` は整数でないIDに新規インスタンスごとにUUIDを付与し、`autoIncrement = true` はその値をデータベースに委ねる。
- `.map.xml` の `<id>` 要素も同じ名前 `autoIncrement`・``autoGenerated` を使う。旧来の `increment`・`generate` 属性で書かれたファイルも引き続き動作する。両方の綴りが存在する場合、新しい方が優先される。
### テーブルの自動作成
`application.properties` に1行追加することで、存在しないテーブルを自動的に作成できます:
```properties
database.autocreate=true
```
- **デフォルトはオフ。** これをオンにしない限り、スキーマは何も変更されません。オンにする場合、データベースユーザーは `CREATE` 権限が必要です。
- マッピングされたクラスが最初にインスタンス化される際に、テーブルが存在しなければ作成されます。これはプロセスごと・クラスごとに一度だけ発生し、アノテーションとXMLマッピングの両方で機能し、以降のインスタンスには何も追加されません。
- `CREATE TABLE IF NOT EXISTS` という単一ステートメントが使用され、複数のインスタンスが同時に起動しても安全です(SQL Server では `OBJECT_ID` ガードが使われます)。
- `@Id` カラムが主キーになり、`autoIncrement = true` の場合はデータベース固有のオートインクリメントまたはIDENTITY構文が使われます。
- カラム型はターゲットデータベース固有の型にレンダリングされます(例:`TIMESTAMP` はMySQLでは `DATETIME`、SQL Serverでは `DATETIME2`、`TEXT` はSQL Serverでは `VARCHAR(MAX)`、`BLOB` はPostgreSQLでは `BYTEA`)。`length` なしの `VARCHAR` は255になります。
- 失敗(例:権限不足)は警告としてログに記録され、オブジェクトの生成は停止しません。最初の実際のステートメントでデータベース固有のエラーが報告されます。
注意点:
- `DECIMAL` カラムはマッピングが単一の `length` のみを持つため、精度やスケールなしで作成されます。
- `FieldType` の名前に含まれないカラム型は拒否されます。
- テーブル、カラム、主キーのみが作成されます。インデックス、外部キー、デフォルト値、他カラムへの `NOT NULL` は作成されません。
- 既存のテーブルはマッピングと異なっていても変更・マイグレーションされません。
- Redisはテーブルを持たず、サポートされていません。
## 重要なルール
1. **ファイルの配置**(XMLマッピングのみ):マッピングXMLは **`src/main/resources/`** 以下にPOJOのパッケージパスを反映した場所に置く必要があります。
2. **命名規則**:テーブル名はクラス名に単数化されます(`users` → `User`)。アンダースコアのカラムはキャメルケースのフィールドになります(`created_at` → `createdAt`)。
3. **セッター**:セッターでは(例:`setFieldAsString`)`setFieldAsXxx` メソッドを使用して内部フィールドマップと状態を同期させてください。
4. **Idフィールド**:Javaでの主キーフィールドは常に `Id` という名前です(`AbstractData` から継承)。
5. **マッピングソースは1つ**:クラスに対してアノテーションかXMLのどちらかを使用してください。両方が存在する場合、アノテーションが優先されてXMLは無視されます。
+135 -14
View File
@@ -1,8 +1,9 @@
---
name: tinystruct-patterns
description: Expert guidance for developing with the tinystruct Java framework. Use when working on the tinystruct codebase or any project built on tinystruct — including creating Application classes, @Action-mapped routes, unit tests, ActionRegistry, HTTP/CLI dual-mode handling, the built-in HTTP server, the event system, JSON with Builder/Builders, database persistence with AbstractData, POJO generation, Server-Sent Events (SSE), file uploads, and outbound HTTP networking.
description: Expert guidance for developing with the tinystruct Java framework. Use when working on the tinystruct codebase or any project built on tinystruct — including generating the bin/dispatcher and bin/dispatcher.cmd launcher scripts when a project lacks them, creating Application classes, @Action-mapped routes, unit tests, ActionRegistry, HTTP/CLI dual-mode handling, the built-in HTTP server, the event system, JSON with Builder/Builders, database persistence with AbstractData, POJO generation, Server-Sent Events (SSE), file uploads, and outbound HTTP networking.
metadata:
origin: ECC
upstream: tinystruct==1.7.34
---
# tinystruct Development Patterns
@@ -13,16 +14,90 @@ Architecture and implementation patterns for building modules with the **tinystr
**CLI and HTTP are equal citizens.** Every method annotated with `@Action` should ideally be runnable from both a terminal and a web browser without modification. This "dual-mode" capability is the core design philosophy of tinystruct.
## Primary Development Tool: `bin/dispatcher`
**`bin/dispatcher` is the default, highest-priority tool for developing, running, testing, and debugging a tinystruct application — reach for it before an IDE run configuration, a hand-written `main()`, curl, or a browser.** Because every `@Action` is dual-mode by design, `bin/dispatcher` lets you exercise routing, argument binding, and business logic directly from the terminal, with the fastest possible feedback loop and no server/browser required.
```bash
# Run any @Action directly - the fastest way to verify a new action works
# (the class must be imported; see --import or default.import.applications in application.properties)
bin/dispatcher greet/James --import com.example.MyService
bin/dispatcher echo --words "Praise the Lord"
# Start the HTTP server when you need the web-facing counterpart
bin/dispatcher start --import org.tinystruct.system.HttpServer
# Import additional Application/MCP classes for the current run
# (one --import per class; a comma-separated list fails with ClassNotFoundException)
bin/dispatcher start --import org.tinystruct.system.HttpServer --import com.example.MyService
# Discover what's available
bin/dispatcher --help
bin/dispatcher --version
```
Default to this workflow: implement the `@Action`, run it immediately via `bin/dispatcher <action>` to confirm it behaves correctly in CLI mode, and only start the HTTP server (also via `bin/dispatcher start ...`) once you need to verify the web-facing path (e.g. `mode = Mode.HTTP_POST`, sessions, file uploads). Never hardcode a `main(String[] args)` as an app's entry point — `bin/dispatcher` (or `bin/dispatcher.cmd` on Windows) is the one entry point for every module.
### Generating `bin/dispatcher` and `bin/dispatcher.cmd`
Every tinystruct project needs its launcher in `bin/`. **The framework generates it — never write it by hand, and never copy it from another project or substitute a `main()`.** `ApplicationManager.init()` writes the launcher for the current operating system into `bin/` under the working directory whenever it is missing, from templates inside the tinystruct jar, with that jar's version filled in. Line endings and the executable bit are already right.
**1. Find the tinystruct version** the project builds against: the `<tinystruct.version>` property or the `org.tinystruct:tinystruct` dependency in `pom.xml`. The jar must be reachable, in `~/.m2/repository/org/tinystruct/tinystruct/<version>/` (run `mvn dependency:resolve` if it is not) or in the project's `lib/`.
**2. From the project root, run the framework once:**
```bash
java -cp ~/.m2/repository/org/tinystruct/tinystruct/<version>/tinystruct-<version>.jar \
org.tinystruct.system.Dispatcher --version
```
Replace `<version>` with the version found in step 1. On Windows use `%USERPROFILE%\.m2\repository\org\tinystruct\tinystruct\<version>\tinystruct-<version>.jar` and the same class. This creates **only the script for the OS you ran it on**, and only if it is not already there:
| Run on | Creates |
|---|---|
| Linux, macOS | `bin/dispatcher` (executable, LF) |
| Windows | `bin\dispatcher.cmd` (CRLF) |
**3. Verify.** From the project root run `bin/dispatcher --version` (Windows: `bin\dispatcher.cmd --version`). It prints `Dispatcher (cli) (built on tinystruct-<version>)`, and the `VERSION` inside the script equals the jar's version. Then `bin/dispatcher --help` lists the commands and every imported action. Show the user the file you created.
**4. Get both scripts by running step 2 on each OS**, or let a teammate or CI on the other OS run it and commit the result. Commit both, so a checkout on either OS works. Do not try to fake the other OS with `-Dos.name=…`: on Windows it fails (`UnsupportedOperationException` from `setPosixFilePermissions`) and leaves an empty `bin/dispatcher`. Recommended `.gitattributes`, so a checkout does not corrupt the line endings:
```
bin/dispatcher text eol=lf
bin/dispatcher.cmd text eol=crlf
```
**Upgrading tinystruct:** change the version in `pom.xml`, delete the old script, and repeat step 2 with the new jar. Alternatively `bin/dispatcher update` checks Maven Central for the latest release, upgrades the project's dependency and regenerates the script with force; it needs the network, edits the `pom.xml`, and goes to the *latest* version, not a chosen one.
**It also happens implicitly.** Any code that reaches `ApplicationManager.init()` (the dispatcher itself, or `ApplicationManager.install(app, config)` in a unit test) creates the script in the *current directory* if it is missing. If unit tests scatter `bin/dispatcher.cmd` into module directories, run them from the build directory: in the surefire configuration set `<workingDirectory>${project.build.directory}</workingDirectory>`.
**What the scripts do**, which explains most problems:
- Run them **from the project root**: the shell script takes the current directory as the root, the `.cmd` takes the parent of `bin/`.
- The classpath is `target/classes`, `lib/*.jar`, `WEB-INF/lib/*`, `WEB-INF/classes` and the tinystruct jar (from `lib/`, else `~/.m2`), then they run `org.tinystruct.system.Dispatcher` with your arguments.
- On the first run, if `mvnw` / `mvnw.cmd` is missing, they extract the Maven Wrapper into the project root (`unzip` on Unix, PowerShell on Windows). Tell the user this happens; the files can be committed or ignored.
- Only the shell script treats `-D…` and `-X…` arguments as JVM options.
**Gotchas:**
- **On Windows use `bin\dispatcher.cmd`.** The shell script joins the classpath with `:`, so under Git Bash/MSYS it fails with `ClassNotFoundException: org.tinystruct.system.Dispatcher`. Use WSL, or the `.cmd`.
- **Only the tinystruct jar is on the classpath.** The framework no longer ships a fat jar by default, so your other modules, JDBC drivers and libraries (jjwt, lettuce, …) must be in `lib/`: `mvn dependency:copy-dependencies -DoutputDirectory=lib` (and git-ignore `lib/`). In a multi-module build, keep the launcher in the directory that holds `target/classes` and `lib/`, and copy the sibling modules' jars into that `lib/`; running it from a module whose siblings are not on its classpath fails with `NoClassDefFoundError`.
- **Load applications with `--import` or configuration.** One `--import` per class (`--import a.A,b.B` fails; write `--import a.A --import b.B`), or list them once in `application.properties`: `default.import.applications=a.A;b.B` (`;`-separated).
- **`JAVA_HOME` must be set** for the `.cmd`; it stops with an error otherwise.
- **`bin/` sits directly under the directory that holds `target/classes` and `lib/`.** The `.cmd` treats the parent of `bin/` as the root, so a `bin/` nested in the wrong directory puts the wrong classpath in front of `Dispatcher`.
## When to Activate
### When to Use
- Running, testing, or debugging any `@Action` via `bin/dispatcher` — this is the default way to work with a tinystruct app, before reaching for HTTP or an IDE run configuration.
- Setting up a project that has no `bin/dispatcher` / `bin/dispatcher.cmd` — have the framework generate it (see "Generating `bin/dispatcher` and `bin/dispatcher.cmd`").
- Creating new `Application` modules by extending `AbstractApplication`.
- Defining routes and command-line actions using `@Action`.
- Handling per-request state via `Context`.
- Performing JSON serialization using the native `Builder` and `Builders` components.
- Working with database persistence via `AbstractData` POJOs.
- Generating POJOs from database tables using the `generate` command.
- Generating POJOs from database tables using the `generate` command, with an XML mapping file or annotations (`--mapping annotation`).
- Implementing Server-Sent Events (SSE) for real-time push.
- Handling file uploads via multipart data.
- Making outbound HTTP requests with `URLRequest` and `HTTPHandler`.
@@ -33,7 +108,7 @@ Architecture and implementation patterns for building modules with the **tinystr
The tinystruct framework treats any method annotated with `@Action` as a routable endpoint for both terminal and web environments. Applications are created by extending `AbstractApplication`, which provides core lifecycle hooks like `init()` and access to the request `Context`.
Routing is handled by the `ActionRegistry`, which automatically maps path segments to method arguments and injects dependencies. For data-only services, the native `Builder` and `Builders` components should be used for JSON serialization to maintain a zero-dependency footprint. The database layer uses `AbstractData` POJOs paired with XML mapping files for CRUD operations without external ORM libraries.
Routing is handled by the `ActionRegistry`, which automatically maps path segments to method arguments and injects dependencies. For data-only services, the native `Builder` and `Builders` components should be used for JSON serialization to maintain a zero-dependency footprint. The database layer uses `AbstractData` POJOs mapped to tables with `@Table`/`@Column` annotations or XML mapping files for CRUD operations without external ORM libraries. Missing tables can be created automatically with `database.autocreate=true`.
## Examples
@@ -125,15 +200,31 @@ public String upload(Request<?, ?> request) throws ApplicationException {
}
```
### Database Metadata Operations
`DatabaseOperator` provides direct access to connection-level catalog, schema, and metadata:
```java
import org.tinystruct.data.DatabaseOperator;
DatabaseOperator operator = new DatabaseOperator();
try {
String catalog = operator.getCatalog();
String schema = operator.getSchema();
java.sql.DatabaseMetaData metaData = operator.getMetaData();
System.out.println("Using catalog: " + catalog + ", schema: " + schema);
} finally {
operator.close();
}
```
## MCP Server and Tools Integration
tinystruct provides native support for the Model Context Protocol (MCP) starting with SDK version **`1.7.26`**.
tinystruct provides native support for the Model Context Protocol (MCP) starting with SDK version **`1.7.0`**.
The MCP APIs (e.g., `org.tinystruct.mcp.MCPTool`, `org.tinystruct.mcp.MCPServer`, `org.tinystruct.mcp.MCPException`) are included directly in the core dependency:
```xml
<dependency>
<groupId>org.tinystruct</groupId>
<artifactId>tinystruct</artifactId>
<version>1.7.26</version>
<version>1.7.34</version>
</dependency>
```
@@ -200,6 +291,11 @@ Run the server via the dispatcher:
bin/dispatcher start --import org.tinystruct.system.HttpServer --import com.example.MyMCPServer
```
### Overloaded Tool Methods
tinystruct supports overloaded methods for the same tool name in `MCPTool` (sharing the same tool name but accepting different parameter signatures).
- **Schema Merging**: Input schemas of all overloads with the same name are dynamically merged into a unified JSON schema. Properties are unioned, and required properties are intersected (only fields required across *all* overloads remain marked mandatory).
- **Execution Routing**: When executing a tool, the server iterates sequentially through the available overloaded methods, attempting execution. Each method's schema validation is performed first, and the first overload that validates against the provided arguments successfully executes. If no signature matches or succeeds, the framework throws an appropriate exception.
## Configuration
Settings are managed in `src/main/resources/application.properties`.
@@ -210,10 +306,13 @@ driver=org.h2.Driver
database.url=jdbc:h2:~/mydb
database.user=sa
database.password=
# Optional: create missing tables from the class mapping on first use (off by default)
# database.autocreate=true
# Server
default.home.page=hello
server.port=8080
default.server.open_browser=true
# Locale
default.language=en_US
@@ -222,6 +321,11 @@ default.language=en_US
# default.session.repository=org.tinystruct.http.RedisSessionRepository
# redis.host=127.0.0.1
# redis.port=6379
# Programmatic Logging Configuration
logging.enabled=true
logging.level=INFO
org.tinystruct.level=FINE
```
Access config values in your application:
@@ -229,6 +333,14 @@ Access config values in your application:
String port = this.getConfiguration("server.port");
```
## Logging and Diagnostics
The framework includes a programmatic wrapper around `java.util.logging` (JUL) that provides beautiful console output and advanced caller tracing out of the box.
- **ANSI Console Colors**: Console output is color-coded based on the log level (red for SEVERE, yellow for WARNING, green for INFO, cyan for CONFIG, and grey for FINE/debug logs).
- **Precise Caller Tracing**: Uses Java's `StackWalker` API to trace the call stack at runtime. It identifies and logs the exact class, method, filename, and line number of the caller that initiated the log (bypassing internal utility and logging layers).
- **Package/Logger Level Overrides**: Set package-specific overrides directly in `application.properties` (e.g., `org.tinystruct.level=FINE`).
## Red Flags & Anti-patterns
| Symptom | Correct Pattern |
@@ -237,7 +349,11 @@ String port = this.getConfiguration("server.port");
| Using `List<Builder>` for JSON arrays | Use `Builders` to avoid generic type erasure issues. |
| `ApplicationRuntimeException: template not found` | Call `setTemplateRequired(false)` in `init()` for API-only apps. |
| Annotating `private` methods with `@Action` | Actions must be `public` to be registered by the framework. |
| Hardcoding `main(String[] args)` in apps | Use `bin/dispatcher` as the entry point for all modules. |
| Hardcoding `main(String[] args)` in apps, or testing only via curl/browser/IDE run configs | Use `bin/dispatcher` as the entry point and default dev/test tool for all modules. |
| `bin/dispatcher` is missing, or a hand-written or copied launcher script | Let the framework generate it: run `org.tinystruct.system.Dispatcher --version` once from the project root (creates the current OS's script). |
| `bin/dispatcher` fails with `ClassNotFoundException: org.tinystruct.system.Dispatcher` on Windows | The shell script cannot run under Git Bash/MSYS; use `bin\dispatcher.cmd` or WSL. |
| `ClassNotFoundException` for `--import a.A,b.B` | One `--import` per class: `--import a.A --import b.B`. |
| A shell-script launcher that fails with `\r: command not found` | The file has CRLF endings; convert `bin/dispatcher` to LF. |
| Manual `ActionRegistry` registration | Prefer the `@Action` annotation for automatic discovery. |
| Action not found at runtime | Ensure class is imported via `--import` or listed in `application.properties`. |
| CLI arg not visible | Pass with `--key value`; access via `getContext().getAttribute("--key")`. |
@@ -245,11 +361,12 @@ String port = this.getConfiguration("server.port");
## Best Practices
1. **Granular Applications**: Break logic into smaller, focused applications rather than one monolithic class.
2. **Setup in `init()`**: Leverage `init()` for setup (config, DB) rather than the constructor. Do NOT call `setAction()` — use `@Action` annotation.
3. **Mode Awareness**: Use the `Mode` parameter in `@Action` to restrict sensitive operations to `CLI` only or specific HTTP methods.
4. **Context over Params**: For optional CLI flags, use `getContext().getAttribute("--flag")` rather than adding parameters to the method signature.
5. **Asynchronous Events**: For heavy tasks triggered by events, use `CompletableFuture.runAsync()` inside the event handler.
1. **`bin/dispatcher` first**: Develop and verify against `bin/dispatcher` before anything else — run the action from the CLI, confirm behavior, then layer on HTTP/mode concerns. It's the fastest inner-loop and the one tool guaranteed to match how the framework actually routes and binds arguments.
2. **Granular Applications**: Break logic into smaller, focused applications rather than one monolithic class.
3. **Setup in `init()`**: Leverage `init()` for setup (config, DB) rather than the constructor. Do NOT call `setAction()` — use `@Action` annotation.
4. **Mode Awareness**: Use the `Mode` parameter in `@Action` to restrict sensitive operations to `CLI` only or specific HTTP methods.
5. **Context over Params**: For optional CLI flags, use `getContext().getAttribute("--flag")` rather than adding parameters to the method signature.
6. **Asynchronous Events**: For heavy tasks triggered by events, use `CompletableFuture.runAsync()` inside the event handler.
## Technical Reference
@@ -258,7 +375,7 @@ Detailed guides are available in the `references/` directory:
- [Architecture & Config](references/architecture.md) — Abstractions, Package Map, Properties
- [Routing & @Action](references/routing.md) — Annotation details, Modes, Parameters
- [Data Handling](references/data-handling.md) — Builder, Builders, JSON serialization & parsing
- [Database Persistence](references/database.md) — AbstractData POJOs, CRUD, mapping XML, POJO generation
- [Database Persistence](references/database.md) — AbstractData POJOs, CRUD, annotation and XML mapping, POJO generation, table auto-creation
- [System & Usage](references/system-usage.md) — Context, Sessions, SSE, File Uploads, Events, Networking
- [Testing Patterns](references/testing.md) — JUnit 5 unit and HTTP integration testing
@@ -270,10 +387,14 @@ Detailed guides are available in the `references/` directory:
- `src/main/java/org/tinystruct/data/component/Builder.java` — JSON object serializer
- `src/main/java/org/tinystruct/data/component/Builders.java` — JSON array serializer
- `src/main/java/org/tinystruct/data/component/AbstractData.java` — Base POJO class with CRUD
- `src/main/java/org/tinystruct/data/Mapping.java` — Mapping XML parser
- `src/main/java/org/tinystruct/data/tools/MySQLGenerator.java` — POJO generator reference
- `src/main/java/org/tinystruct/data/Mapping.java` — Mapping metadata (annotations or XML), cached per class
- `src/main/java/org/tinystruct/data/annotation/Table.java` — `@Table`, with `@Id` and `@Column` alongside it
- `src/main/java/org/tinystruct/data/tools/TableCreator.java` — Creates missing tables (`database.autocreate`)
- `src/main/java/org/tinystruct/data/tools/MySQLGenerator.java` — POJO generator reference (`MappingMode` selects XML or annotations)
- `src/main/java/org/tinystruct/data/component/FieldType.java` — SQL-to-Java type mappings
- `src/main/java/org/tinystruct/data/component/Condition.java` — Fluent SQL query builder
- `src/main/java/org/tinystruct/http/SSEPushManager.java` — SSE connection management
- `src/main/java/org/tinystruct/system/logging/LogFormatter.java` — Custom log formatter with ANSI console colors and StackWalker-based caller tracing
- `src/main/java/org/tinystruct/system/logging/LoggerConfigurer.java` — Programmatic logging configurator from application properties
- `src/test/java/org/tinystruct/application/ActionRegistryTest.java` — Registry test examples
- `src/test/java/org/tinystruct/system/HttpServerHttpModeTest.java` — HTTP integration test patterns
@@ -46,7 +46,9 @@ org.tinystruct/
│ ├── component/AbstractData.java ← base POJO for DB persistence
│ ├── component/Condition.java ← fluent SQL query builder
│ ├── component/FieldType.java ← SQL-to-Java type mappings
│ ├── Mapping.java ← reads .map.xml metadata
│ ├── annotation/ ← @Table, @Id, @Column mapping annotations
│ ├── Mapping.java ← reads @Table/@Column annotations or .map.xml metadata, cached per class
│ ├── tools/TableCreator.java ← creates missing tables (database.autocreate)
│ ├── DatabaseOperator.java ← low-level JDBC wrapper
│ └── FileEntity.java ← file upload representation
├── http/ ← Request, Response, Constants
@@ -2,7 +2,7 @@
## When to Use
Use the built-in ORM-like data layer for database operations. It provides a lightweight alternative to JPA/Hibernate using POJOs extending `AbstractData` and XML mapping files.
Use the built-in ORM-like data layer for database operations. It provides a lightweight alternative to JPA/Hibernate using POJOs extending `AbstractData`, mapped to tables with annotations or XML mapping files.
## How It Works
@@ -10,7 +10,11 @@ Use the built-in ORM-like data layer for database operations. It provides a ligh
Each table is represented by:
1. **Java POJO**: Extends `AbstractData`, provides getters/setters and `setData(Row)`.
2. **Mapping XML**: `ClassName.map.xml` in resources, binding Java fields to DB columns.
2. **Mapping**, either of:
- **Annotations**: `@Table` on the class and `@Column` on its fields (see [Annotation Mapping](#annotation-mapping)).
- **Mapping XML**: `ClassName.map.xml` in resources, binding Java fields to DB columns.
The mapping is read once per class and cached, so creating an instance never re-reads or re-parses it, whichever form you use. If a class has `@Table`, it wins over a `.map.xml` of the same name.
#### Key Base Class: `AbstractData`
Provides CRUD methods:
@@ -23,7 +27,7 @@ Provides CRUD methods:
### POJO Generation (CLI)
Introspect a live database table to produce the POJO and mapping file.
Introspect a live database table to produce the POJO and its mapping: a `.map.xml` file by default, or annotations on the POJO itself.
#### Configuration
`application.properties`:
@@ -41,8 +45,13 @@ bin/dispatcher generate
# Specify table
bin/dispatcher generate --tables users
# Annotate the POJO instead of writing a .map.xml file
bin/dispatcher generate --tables users --mapping annotation
```
`--mapping` accepts `xml` (the default) or `annotation`. In annotation mode no `.map.xml` is written and no resources directory is created. An unknown value is rejected before any prompt.
## Examples
### CRUD Operations
@@ -84,16 +93,70 @@ Table filtered = user.find(
```xml
<mapping>
<class name="User" table="users">
<id name="Id" column="id" increment="true" generate="false" length="11" type="int"/>
<id name="Id" column="id" autoIncrement="true" autoGenerated="false" length="11" type="int"/>
<property name="username" column="username" length="50" type="varchar"/>
<property name="email" column="email" length="100" type="varchar"/>
</class>
</mapping>
```
### Annotation Mapping
The same `User` mapped with annotations (this is what `generate --mapping annotation` produces):
```java
import org.tinystruct.data.annotation.Column;
import org.tinystruct.data.annotation.Id;
import org.tinystruct.data.annotation.Table;
@Table(name = "users",
id = @Id(name = "Id", column = "id", type = "int", length = 11, autoIncrement = true))
public class User extends AbstractData implements Serializable {
@Column(name = "username", type = "varchar", length = 50)
private String username;
@Column(name = "email", type = "varchar", length = 100)
private String email;
// getters, setters (using setFieldAsXxx) and setData(Row) as usual
}
```
| Annotation | On | Attributes |
|---|---|---|
| `@Table` | class | `name` (required), `schema` (default none), `id` (default: no identifier) |
| `@Id` | inside `@Table(id = ...)` | `name` (default `Id`), `column`, `type`, `length`, `autoIncrement`, `autoGenerated`. An empty `column` means the table has no identifier. |
| `@Column` | field | `type` (required), `name` (the DB column; defaults to the Java field name), `length` |
- The identifier is described inside `@Table` because it is inherited from `AbstractData`; there is no field of your own to annotate.
- `type` takes the same names as the XML `type` attribute (the `FieldType` names, matched case-insensitively): `INT`, `BIGINT`, `VARCHAR`, `TEXT`, `DATE`, `TIMESTAMP` and so on.
- Only fields with `@Column` are mapped. `@Table` is inherited, and the annotated fields of superclasses are mapped too.
- Mapping the same property twice, for example a subclass field that hides an annotated superclass field, fails with an error the first time the class is used.
- `autoGenerated = true` gives a non-integer id a fresh UUID for every new instance; `autoIncrement = true` leaves the value to the database.
- The `<id>` element of a `.map.xml` file uses the same names, `autoIncrement` and `autoGenerated`. Files written with the original `increment` and `generate` attributes keep working; if both spellings are present, the new one wins.
### Auto-Creating Tables
Add one line to `application.properties` to have missing tables created for you:
```properties
database.autocreate=true
```
- **Off by default.** Nothing touches your schema unless you turn this on, and the database user then needs the `CREATE` privilege.
- The first time a mapped class is instantiated, its table is created if it does not exist. This happens once per class per process, works for annotation and XML mappings alike, and adds nothing to later instances.
- It is a single statement that is safe when several instances start together: `CREATE TABLE IF NOT EXISTS` (SQL Server uses an `OBJECT_ID` guard instead).
- The `@Id` column becomes the primary key, with the database's own auto-increment or identity syntax when `autoIncrement = true`.
- Column types are rendered as the target database's native type, for example `TIMESTAMP` becomes `DATETIME` on MySQL and `DATETIME2` on SQL Server, `TEXT` becomes `VARCHAR(MAX)` on SQL Server, and `BLOB` becomes `BYTEA` on PostgreSQL. A `VARCHAR` without a length gets 255.
- A failure (for example missing privileges) is logged as a warning and never stops the object from being created; the first real statement then reports the database's own error.
Limits to keep in mind:
- `DECIMAL` columns are created without precision or scale, because the mapping carries only a single `length`.
- A column type that is not one of the `FieldType` names is rejected.
- Only tables, columns and the primary key are created. There are no indexes, foreign keys, defaults or `NOT NULL` on other columns.
- An existing table is never altered or migrated, even if it differs from the mapping.
- Redis has no tables and is not supported.
## Important Rules
1. **File Placement**: The mapping XML **must** mirror the POJO's package path under `src/main/resources/`.
1. **File Placement** (XML mapping only): The mapping XML **must** mirror the POJO's package path under `src/main/resources/`.
2. **Naming**: Table names are singularized for class names (`users` → `User`). Underscored columns become camelCase fields (`created_at` → `createdAt`).
3. **Setters**: Use `setFieldAsXxx` methods (e.g., `setFieldAsString`) in setters to sync state with the internal field map.
4. **Id Field**: The primary key field in Java is always named `Id` (inherited from `AbstractData`).
5. **One Mapping Source**: Use annotations or XML for a class, not both. If both exist the annotation wins and the XML is ignored.