@archznn/xavva 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -6
- package/package.json +1 -1
- package/src/commands/Command.ts +1 -1
- package/src/commands/CommandRegistry.ts +3 -3
- package/src/commands/DeployCommand.ts +4 -3
- package/src/commands/HelpCommand.ts +6 -3
- package/src/commands/TomcatCommand.ts +105 -10
- package/src/index.ts +1 -1
- package/src/services/BuildService.ts +105 -3
- package/src/services/EmbeddedTomcatService.ts +415 -370
- package/src/services/WatcherService.ts +41 -3
- package/src/types/config.ts +1 -0
- package/src/utils/config.ts +3 -1
- package/src/utils/constants.ts +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Ultra-fast development toolkit for Java Enterprise (Tomcat) on Windows
|
|
4
4
|
|
|
5
|
-
[](https://github.com/leorsousa05/Xavva)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
Xavva is a high-performance CLI built with **Bun** that transforms the Java/Tomcat development experience. It brings modern development workflows (like Node.js/Vite) to the Java Enterprise ecosystem with hot-reload, smart logging, and automated deployment.
|
|
@@ -102,16 +102,25 @@ Xavva can automatically download and manage a Tomcat installation for you:
|
|
|
102
102
|
# First time usage - auto-install Tomcat
|
|
103
103
|
xavva dev --yes
|
|
104
104
|
|
|
105
|
-
#
|
|
106
|
-
xavva tomcat
|
|
105
|
+
# List available versions to download
|
|
106
|
+
xavva tomcat list
|
|
107
|
+
|
|
108
|
+
# List already installed versions
|
|
109
|
+
xavva tomcat installed
|
|
110
|
+
|
|
111
|
+
# Install a specific version
|
|
112
|
+
xavva tomcat install 9.0.115
|
|
113
|
+
|
|
114
|
+
# Switch to a version for this project
|
|
115
|
+
xavva tomcat use 9.0.115
|
|
107
116
|
|
|
108
117
|
# Check Tomcat status
|
|
109
118
|
xavva tomcat status
|
|
110
119
|
|
|
111
|
-
#
|
|
112
|
-
xavva tomcat
|
|
120
|
+
# Remove a version
|
|
121
|
+
xavva tomcat uninstall 9.0.115
|
|
113
122
|
|
|
114
|
-
#
|
|
123
|
+
# Or use with flags
|
|
115
124
|
xavva dev --tomcat-version 9.0.115
|
|
116
125
|
```
|
|
117
126
|
|
package/package.json
CHANGED
package/src/commands/Command.ts
CHANGED
|
@@ -19,18 +19,18 @@ export class CommandRegistry {
|
|
|
19
19
|
return this.commands.get(name);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
async execute(name: string, config: AppConfig, args: CLIArguments): Promise<void> {
|
|
22
|
+
async execute(name: string, config: AppConfig, args: CLIArguments, positionals?: string[]): Promise<void> {
|
|
23
23
|
const command = this.commands.get(name);
|
|
24
24
|
const processManager = ProcessManager.getInstance();
|
|
25
25
|
|
|
26
26
|
if (!command) {
|
|
27
27
|
Logger.error(`Comando desconhecido: ${name}`);
|
|
28
|
-
await new HelpCommand().execute(config);
|
|
28
|
+
await new HelpCommand().execute(config, args, positionals);
|
|
29
29
|
await processManager.shutdown(2);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
try {
|
|
33
|
-
await command.execute(config, args);
|
|
33
|
+
await command.execute(config, args, positionals);
|
|
34
34
|
} catch (error) {
|
|
35
35
|
const message = error instanceof Error ? error.message : String(error);
|
|
36
36
|
Logger.error(`Erro ao executar comando '${name}': ${message}`);
|
|
@@ -14,6 +14,7 @@ export class DeployCommand implements Command {
|
|
|
14
14
|
async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
|
|
15
15
|
const incremental = args?.watch && args?.incremental;
|
|
16
16
|
const isWatching = !!args?.watch;
|
|
17
|
+
const changedFiles = args?.changedFiles;
|
|
17
18
|
const tomcat = this.tomcat;
|
|
18
19
|
const builder = this.builder;
|
|
19
20
|
|
|
@@ -46,11 +47,11 @@ export class DeployCommand implements Command {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
if (incremental) {
|
|
49
|
-
const actualAppFolder = await builder.syncClasses();
|
|
50
|
+
const actualAppFolder = await builder.syncClasses(changedFiles);
|
|
50
51
|
const actualContextPath = contextPath || actualAppFolder || "";
|
|
51
52
|
const actualAppUrl = `http://localhost:${config.tomcat.port}/${actualContextPath}`;
|
|
52
53
|
await BrowserService.reload(actualAppUrl);
|
|
53
|
-
Logger.success(
|
|
54
|
+
Logger.success(`redeploy completed (${changedFiles?.length || 'all'} file(s))`);
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -98,7 +99,7 @@ export class DeployCommand implements Command {
|
|
|
98
99
|
const finalAppUrl = `http://localhost:${config.tomcat.port}/${finalContextPath}`;
|
|
99
100
|
|
|
100
101
|
tomcat.onReady = async () => {
|
|
101
|
-
await this.handleServerReady(config, finalAppUrl, finalContextPath, tomcat, incremental);
|
|
102
|
+
await this.handleServerReady(config, finalAppUrl, finalContextPath, tomcat, !!incremental);
|
|
102
103
|
};
|
|
103
104
|
|
|
104
105
|
tomcat.start(config, isWatching);
|
|
@@ -28,7 +28,7 @@ export class HelpCommand implements Command {
|
|
|
28
28
|
${this.c("green", "audit")} Security audit of JAR files
|
|
29
29
|
${this.c("green", "doctor")} Diagnose and fix environment issues
|
|
30
30
|
${this.c("green", "profiles")} List available Maven/Gradle profiles
|
|
31
|
-
${this.c("green", "tomcat")} Manage embedded Tomcat (install, list, status)
|
|
31
|
+
${this.c("green", "tomcat")} Manage embedded Tomcat (install, list, installed, use, status)
|
|
32
32
|
${this.c("green", "docs")} Generate endpoint documentation
|
|
33
33
|
|
|
34
34
|
${this.c("yellow", "GENERAL OPTIONS")}
|
|
@@ -90,9 +90,12 @@ export class HelpCommand implements Command {
|
|
|
90
90
|
xavva audit --fix
|
|
91
91
|
|
|
92
92
|
${this.c("dim", "# Manage embedded Tomcat")}
|
|
93
|
-
xavva tomcat
|
|
93
|
+
xavva tomcat list # List available versions
|
|
94
|
+
xavva tomcat installed # List installed versions
|
|
95
|
+
xavva tomcat install 9.0.115 # Install specific version
|
|
96
|
+
xavva tomcat use 9.0.115 # Switch to version for this project
|
|
94
97
|
xavva tomcat status
|
|
95
|
-
xavva tomcat
|
|
98
|
+
xavva tomcat uninstall 9.0.115
|
|
96
99
|
|
|
97
100
|
${this.c("yellow", "CONFIGURATION")}
|
|
98
101
|
Settings are loaded from ${this.c("cyan", "xavva.json")} in the project root:
|
|
@@ -3,32 +3,47 @@ import type { AppConfig, CLIArguments } from "../types/config";
|
|
|
3
3
|
import { EmbeddedTomcatService } from "../services/EmbeddedTomcatService";
|
|
4
4
|
import { Logger } from "../utils/ui";
|
|
5
5
|
import path from "path";
|
|
6
|
+
import fs from "fs";
|
|
6
7
|
|
|
7
8
|
export class TomcatCommand implements Command {
|
|
8
|
-
async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
|
|
9
|
-
|
|
9
|
+
async execute(config: AppConfig, args?: CLIArguments, positionals?: string[]): Promise<void> {
|
|
10
|
+
// A ação vem como positional após "tomcat" (ex: xavva tomcat list)
|
|
11
|
+
const tomcatIndex = positionals?.indexOf("tomcat") ?? -1;
|
|
12
|
+
const action = positionals && tomcatIndex >= 0 && positionals[tomcatIndex + 1]
|
|
13
|
+
? positionals[tomcatIndex + 1]
|
|
14
|
+
: "status";
|
|
15
|
+
|
|
16
|
+
// Argumentos extras após a ação (ex: xavva tomcat install 9.0.115)
|
|
17
|
+
const extraArgs = positionals && tomcatIndex >= 0 ? positionals.slice(tomcatIndex + 2) : [];
|
|
10
18
|
|
|
11
19
|
switch (action) {
|
|
12
20
|
case "install":
|
|
13
|
-
await this.handleInstall(config, args);
|
|
21
|
+
await this.handleInstall(config, args, extraArgs);
|
|
14
22
|
break;
|
|
15
23
|
case "list":
|
|
16
24
|
this.handleList();
|
|
17
25
|
break;
|
|
26
|
+
case "installed":
|
|
27
|
+
this.handleInstalled();
|
|
28
|
+
break;
|
|
29
|
+
case "use":
|
|
30
|
+
await this.handleUse(config, args, extraArgs);
|
|
31
|
+
break;
|
|
18
32
|
case "uninstall":
|
|
19
|
-
await this.handleUninstall(config, args);
|
|
33
|
+
await this.handleUninstall(config, args, extraArgs);
|
|
20
34
|
break;
|
|
21
35
|
case "status":
|
|
22
36
|
await this.handleStatus(config);
|
|
23
37
|
break;
|
|
24
38
|
default:
|
|
25
39
|
Logger.error(`Ação desconhecida: ${action}`);
|
|
26
|
-
Logger.info("Ações disponíveis", "install, list, uninstall, status");
|
|
40
|
+
Logger.info("Ações disponíveis", "install, list, installed, use, uninstall, status");
|
|
27
41
|
}
|
|
28
42
|
}
|
|
29
43
|
|
|
30
|
-
private async handleInstall(config: AppConfig, args?: CLIArguments): Promise<void> {
|
|
31
|
-
|
|
44
|
+
private async handleInstall(config: AppConfig, args?: CLIArguments, extraArgs: string[] = []): Promise<void> {
|
|
45
|
+
// Versão pode vir de: flag --tomcat-version, argumento posicional, config, ou padrão
|
|
46
|
+
const version = args?.["tomcat-version"] || extraArgs[0] || config.tomcat.version || "10.1.52";
|
|
32
47
|
|
|
33
48
|
// Detectar webapp path
|
|
34
49
|
const webappPath = config.project.buildTool === "maven"
|
|
@@ -57,7 +72,7 @@ export class TomcatCommand implements Command {
|
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
private handleList(): void {
|
|
60
|
-
Logger.section("Versões Disponíveis");
|
|
75
|
+
Logger.section("Versões Disponíveis para Download");
|
|
61
76
|
const versions = EmbeddedTomcatService.getAvailableVersions();
|
|
62
77
|
|
|
63
78
|
for (const version of versions) {
|
|
@@ -66,10 +81,90 @@ export class TomcatCommand implements Command {
|
|
|
66
81
|
|
|
67
82
|
Logger.newline();
|
|
68
83
|
Logger.info("Versão padrão", "10.1.52");
|
|
84
|
+
Logger.newline();
|
|
85
|
+
Logger.info("Dica", "Use 'xavva tomcat installed' para ver versões já instaladas");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private handleInstalled(): void {
|
|
89
|
+
const installed = EmbeddedTomcatService.listInstalledVersions();
|
|
90
|
+
|
|
91
|
+
Logger.section("Versões Instaladas");
|
|
92
|
+
|
|
93
|
+
if (installed.length === 0) {
|
|
94
|
+
Logger.warn("Nenhuma versão instalada");
|
|
95
|
+
Logger.info("Dica", "Use 'xavva tomcat install <version>' para instalar");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const version of installed) {
|
|
100
|
+
Logger.log(` ${Logger.C.success}✓${Logger.C.reset} ${version}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
Logger.newline();
|
|
104
|
+
Logger.info("Para usar uma versão", "xavva tomcat use <version>");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async handleUse(config: AppConfig, args?: CLIArguments, extraArgs: string[] = []): Promise<void> {
|
|
108
|
+
const version = extraArgs[0] || args?.["tomcat-version"];
|
|
109
|
+
|
|
110
|
+
if (!version) {
|
|
111
|
+
Logger.error("Versão não especificada");
|
|
112
|
+
Logger.info("Uso", "xavva tomcat use <version>");
|
|
113
|
+
Logger.info("Exemplo", "xavva tomcat use 9.0.115");
|
|
114
|
+
Logger.newline();
|
|
115
|
+
Logger.info("Versões instaladas", "");
|
|
116
|
+
this.handleInstalled();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Verifica se a versão está instalada
|
|
121
|
+
const service = new EmbeddedTomcatService({
|
|
122
|
+
version,
|
|
123
|
+
port: config.tomcat.port,
|
|
124
|
+
webappPath: "."
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (!service.checkInstallation()) {
|
|
128
|
+
Logger.warn(`Tomcat ${version} não está instalado`);
|
|
129
|
+
Logger.newline();
|
|
130
|
+
Logger.info("Opções", "");
|
|
131
|
+
Logger.log(` ${Logger.C.primary}1.${Logger.C.reset} Instalar agora: xavva tomcat install ${version}`);
|
|
132
|
+
Logger.log(` ${Logger.C.primary}2.${Logger.C.reset} Ver instaladas: xavva tomcat installed`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Salva a versão no xavva.json do projeto
|
|
137
|
+
await this.saveTomcatVersion(version);
|
|
138
|
+
|
|
139
|
+
Logger.success(`Tomcat ${version} configurado para este projeto!`);
|
|
140
|
+
Logger.newline();
|
|
141
|
+
Logger.info("Próximos comandos", "");
|
|
142
|
+
Logger.log(` ${Logger.C.primary}•${Logger.C.reset} xavva dev # Iniciar desenvolvimento`);
|
|
143
|
+
Logger.log(` ${Logger.C.primary}•${Logger.C.reset} xavva deploy # Fazer deploy`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async saveTomcatVersion(version: string): Promise<void> {
|
|
147
|
+
const xavvaJsonPath = path.join(process.cwd(), "xavva.json");
|
|
148
|
+
let config: any = {};
|
|
149
|
+
|
|
150
|
+
if (fs.existsSync(xavvaJsonPath)) {
|
|
151
|
+
try {
|
|
152
|
+
config = JSON.parse(fs.readFileSync(xavvaJsonPath, "utf-8"));
|
|
153
|
+
} catch (e) {
|
|
154
|
+
// Arquivo existe mas é inválido, começa do zero
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!config.tomcat) config.tomcat = {};
|
|
159
|
+
config.tomcat.version = version;
|
|
160
|
+
config.tomcat.embedded = true;
|
|
161
|
+
|
|
162
|
+
fs.writeFileSync(xavvaJsonPath, JSON.stringify(config, null, 2));
|
|
69
163
|
}
|
|
70
164
|
|
|
71
|
-
private async handleUninstall(config: AppConfig, args?: CLIArguments): Promise<void> {
|
|
72
|
-
|
|
165
|
+
private async handleUninstall(config: AppConfig, args?: CLIArguments, extraArgs: string[] = []): Promise<void> {
|
|
166
|
+
// Versão pode vir de: flag --tomcat-version, argumento posicional, config, ou padrão
|
|
167
|
+
const version = args?.["tomcat-version"] || extraArgs[0] || config.tomcat.version || "10.1.52";
|
|
73
168
|
|
|
74
169
|
const service = new EmbeddedTomcatService({
|
|
75
170
|
version,
|
package/src/index.ts
CHANGED
|
@@ -105,7 +105,7 @@ async function main() {
|
|
|
105
105
|
if (commandName === "debug") values.debug = true;
|
|
106
106
|
if (commandName === "run") values.debug = false;
|
|
107
107
|
|
|
108
|
-
await registry.execute(commandName, config, values);
|
|
108
|
+
await registry.execute(commandName, config, values, positionals);
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -129,19 +129,121 @@ export class BuildService {
|
|
|
129
129
|
await this.fastSync(srcDir, destDir);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
async syncClasses(
|
|
132
|
+
async syncClasses(changedFiles?: string[]): Promise<string | null> {
|
|
133
133
|
const appFolder = this.projectService.getInferredAppName();
|
|
134
134
|
const webappPath = path.join(this.tomcatConfig.path, "webapps", appFolder);
|
|
135
135
|
const targetLib = path.join(webappPath, "WEB-INF", "classes");
|
|
136
|
-
const sourceDir =
|
|
136
|
+
const sourceDir = this.projectService.getClassesDir();
|
|
137
137
|
|
|
138
138
|
if (!existsSync(sourceDir)) return null;
|
|
139
139
|
if (!existsSync(targetLib)) mkdirSync(targetLib, { recursive: true });
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
// Se temos uma lista específica de arquivos modificados, sincroniza apenas eles
|
|
142
|
+
if (changedFiles && changedFiles.length > 0) {
|
|
143
|
+
await this.syncSpecificFiles(changedFiles, sourceDir, targetLib);
|
|
144
|
+
} else {
|
|
145
|
+
// Caso contrário, sincroniza tudo (comportamento padrão)
|
|
146
|
+
await this.fastSync(sourceDir, targetLib);
|
|
147
|
+
}
|
|
148
|
+
|
|
142
149
|
return appFolder;
|
|
143
150
|
}
|
|
144
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Sincroniza apenas arquivos específicos baseado nos arquivos .java modificados.
|
|
154
|
+
* Converte .java para .class e sincroniza apenas os arquivos realmente modificados.
|
|
155
|
+
*/
|
|
156
|
+
private async syncSpecificFiles(changedFiles: string[], sourceDir: string, targetLib: string): Promise<void> {
|
|
157
|
+
const tasks: Promise<void>[] = [];
|
|
158
|
+
const syncedCount = { value: 0 };
|
|
159
|
+
|
|
160
|
+
for (const javaFile of changedFiles) {
|
|
161
|
+
// Converte caminho do .java para caminho do .class
|
|
162
|
+
// Ex: src/main/java/com/example/Foo.java -> target/classes/com/example/Foo.class
|
|
163
|
+
const relativePath = this.javaToClassPath(javaFile);
|
|
164
|
+
if (!relativePath) continue;
|
|
165
|
+
|
|
166
|
+
const sourcePath = path.join(sourceDir, relativePath);
|
|
167
|
+
const targetPath = path.join(targetLib, relativePath);
|
|
168
|
+
|
|
169
|
+
if (!existsSync(sourcePath)) {
|
|
170
|
+
// Se o .class não existe, talvez seja um arquivo excluído ou inner class
|
|
171
|
+
// Neste caso, faz sync completo como fallback
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
tasks.push((async () => {
|
|
176
|
+
const targetDir = path.dirname(targetPath);
|
|
177
|
+
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
178
|
+
|
|
179
|
+
const srcStat = statSync(sourcePath);
|
|
180
|
+
const destStat = existsSync(targetPath) ? statSync(targetPath) : null;
|
|
181
|
+
|
|
182
|
+
if (!destStat || srcStat.mtimeMs > destStat.mtimeMs) {
|
|
183
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
184
|
+
syncedCount.value++;
|
|
185
|
+
}
|
|
186
|
+
})());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await Promise.all(tasks);
|
|
190
|
+
|
|
191
|
+
// Se não conseguimos sincronizar nenhum arquivo específico, faz sync completo
|
|
192
|
+
if (syncedCount.value === 0) {
|
|
193
|
+
await this.fastSync(sourceDir, targetLib);
|
|
194
|
+
} else if (!this.projectConfig.quiet) {
|
|
195
|
+
Logger.info("sync", `${syncedCount.value} classe(s) sincronizada(s)`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Converte caminho de arquivo .java para caminho relativo de .class
|
|
201
|
+
*/
|
|
202
|
+
private javaToClassPath(javaFile: string): string | null {
|
|
203
|
+
// Remove prefixos comuns de diretórios source
|
|
204
|
+
const parts = javaFile.split(/[/\\]/);
|
|
205
|
+
|
|
206
|
+
// Encontra o índice após "java" ou "src/main/java" ou "src"
|
|
207
|
+
let startIndex = -1;
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < parts.length; i++) {
|
|
210
|
+
if (parts[i] === "java" && i > 0 && (parts[i-1] === "main" || parts[i-1] === "test")) {
|
|
211
|
+
startIndex = i + 1;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Se não encontrou padrão maven, tenta achar "src"
|
|
217
|
+
if (startIndex === -1) {
|
|
218
|
+
const srcIndex = parts.indexOf("src");
|
|
219
|
+
if (srcIndex !== -1 && srcIndex < parts.length - 1) {
|
|
220
|
+
// Pula "src" e possível "main/java"
|
|
221
|
+
if (parts[srcIndex + 1] === "main" && parts[srcIndex + 2] === "java") {
|
|
222
|
+
startIndex = srcIndex + 3;
|
|
223
|
+
} else {
|
|
224
|
+
startIndex = srcIndex + 1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Se ainda não encontrou, assume que o caminho já é relativo ao package
|
|
230
|
+
if (startIndex === -1) {
|
|
231
|
+
startIndex = 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Pega o caminho relativo
|
|
235
|
+
const relativeParts = parts.slice(startIndex);
|
|
236
|
+
if (relativeParts.length === 0) return null;
|
|
237
|
+
|
|
238
|
+
// Substitui extensão .java por .class
|
|
239
|
+
const fileName = relativeParts[relativeParts.length - 1];
|
|
240
|
+
if (!fileName || !fileName.endsWith(".java")) return null;
|
|
241
|
+
|
|
242
|
+
relativeParts[relativeParts.length - 1] = fileName.replace(".java", ".class");
|
|
243
|
+
|
|
244
|
+
return path.join(...relativeParts);
|
|
245
|
+
}
|
|
246
|
+
|
|
145
247
|
private async fastSync(src: string, dest: string) {
|
|
146
248
|
const entries = readdirSync(src, { withFileTypes: true });
|
|
147
249
|
|