@archznn/xavva 2.5.0 → 2.7.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 +80 -4
- package/package.json +4 -2
- package/src/commands/DeployCommand.ts +29 -16
- package/src/commands/DoctorCommand.ts +129 -76
- package/src/commands/EncodingCommand.ts +351 -0
- package/src/commands/HelpCommand.ts +15 -0
- package/src/commands/LogsCommand.ts +1 -1
- package/src/commands/RunCommand.ts +21 -11
- package/src/config/versions.ts +63 -0
- package/src/di/container.ts +226 -0
- package/src/errors/ErrorHandler.ts +249 -0
- package/src/errors/XavvaError.ts +273 -0
- package/src/index.ts +98 -96
- package/src/services/AuditService.ts +3 -2
- package/src/services/BrowserService.ts +131 -17
- package/src/services/BuildService.ts +29 -2
- package/src/services/DeployWatcher.ts +183 -0
- package/src/services/EmbeddedTomcatService.ts +48 -53
- package/src/services/EncodingService.ts +548 -0
- package/src/services/FileWatcher.ts +243 -0
- package/src/services/LogAnalyzer.ts +4 -4
- package/src/services/TomcatService.ts +94 -17
- package/src/types/args.ts +136 -0
- package/src/types/config.ts +6 -0
- package/src/types/index.ts +7 -0
- package/src/utils/PathUtils.ts +221 -0
- package/src/utils/config.ts +6 -0
- package/src/utils/parsers/JavaParser.ts +413 -0
- package/src/utils/platform.ts +323 -0
- package/src/services/WatcherService.ts +0 -117
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilitários para manipulação de paths
|
|
3
|
+
* Centraliza lógica comum de normalização e resolução de caminhos
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from "path";
|
|
7
|
+
|
|
8
|
+
export class PathUtils {
|
|
9
|
+
/**
|
|
10
|
+
* Normaliza separadores de path para forward slash
|
|
11
|
+
*/
|
|
12
|
+
static normalizeSeparators(filePath: string): string {
|
|
13
|
+
return filePath.replace(/\\/g, "/");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Converte caminho relativo para absoluto a partir do cwd
|
|
18
|
+
*/
|
|
19
|
+
static resolveFromCwd(relativePath: string): string {
|
|
20
|
+
return path.resolve(process.cwd(), relativePath);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Obtém caminho relativo a partir do diretório raiz do projeto
|
|
25
|
+
*/
|
|
26
|
+
static relativeToProject(absolutePath: string): string {
|
|
27
|
+
return path.relative(process.cwd(), absolutePath);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Verifica se caminho está dentro do diretório src/main/java (Maven) ou equivalente
|
|
32
|
+
*/
|
|
33
|
+
static isSourceFile(filePath: string): boolean {
|
|
34
|
+
const normalized = this.normalizeSeparators(filePath);
|
|
35
|
+
return /src\/(main|test)\/(java|resources)/.test(normalized);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extrai package de um arquivo Java baseado no caminho
|
|
40
|
+
*/
|
|
41
|
+
static extractPackageFromPath(javaFilePath: string): string | null {
|
|
42
|
+
const normalized = this.normalizeSeparators(javaFilePath);
|
|
43
|
+
|
|
44
|
+
// Procura por src/main/java ou src/test/java
|
|
45
|
+
const match = normalized.match(/src\/(?:main|test)\/java\/(.*)\/[^/]+\.java$/);
|
|
46
|
+
if (match) {
|
|
47
|
+
return match[1].replace(/\//g, ".");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Converte caminho de .java para .class
|
|
55
|
+
*/
|
|
56
|
+
static javaToClassPath(javaPath: string): string {
|
|
57
|
+
return javaPath.replace(/\.java$/i, ".class");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Converte caminho relativo do source para caminho no diretório de classes
|
|
62
|
+
*/
|
|
63
|
+
static sourceToClassesPath(
|
|
64
|
+
sourcePath: string,
|
|
65
|
+
classesDir: string,
|
|
66
|
+
sourceRoots: string[] = ["src/main/java", "src/test/java", "src"]
|
|
67
|
+
): string {
|
|
68
|
+
const normalized = this.normalizeSeparators(sourcePath);
|
|
69
|
+
|
|
70
|
+
for (const root of sourceRoots) {
|
|
71
|
+
const normalizedRoot = this.normalizeSeparators(root);
|
|
72
|
+
const index = normalized.indexOf(normalizedRoot);
|
|
73
|
+
if (index !== -1) {
|
|
74
|
+
const relativePart = normalized.slice(index + normalizedRoot.length + 1);
|
|
75
|
+
const classFile = this.javaToClassPath(relativePart);
|
|
76
|
+
return path.join(classesDir, classFile);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fallback: assume que é relativo ao classesDir
|
|
81
|
+
return path.join(classesDir, path.basename(this.javaToClassPath(sourcePath)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Encontra o webapp root a partir de um caminho de recurso
|
|
86
|
+
*/
|
|
87
|
+
static findWebappRoot(resourcePath: string): string | null {
|
|
88
|
+
const normalized = this.normalizeSeparators(resourcePath);
|
|
89
|
+
const parts = normalized.split("/");
|
|
90
|
+
|
|
91
|
+
const webappIndex = parts.indexOf("webapp");
|
|
92
|
+
const webContentIndex = parts.indexOf("WebContent");
|
|
93
|
+
const rootIndex = webappIndex !== -1 ? webappIndex : webContentIndex;
|
|
94
|
+
|
|
95
|
+
if (rootIndex !== -1) {
|
|
96
|
+
return parts.slice(0, rootIndex + 1).join("/");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Obtém caminho relativo dentro do webapp
|
|
104
|
+
*/
|
|
105
|
+
static getWebappRelativePath(resourcePath: string): string | null {
|
|
106
|
+
const normalized = this.normalizeSeparators(resourcePath);
|
|
107
|
+
const parts = normalized.split("/");
|
|
108
|
+
|
|
109
|
+
const webappIndex = parts.indexOf("webapp");
|
|
110
|
+
const webContentIndex = parts.indexOf("WebContent");
|
|
111
|
+
const rootIndex = webappIndex !== -1 ? webappIndex : webContentIndex;
|
|
112
|
+
|
|
113
|
+
if (rootIndex !== -1 && rootIndex < parts.length - 1) {
|
|
114
|
+
return parts.slice(rootIndex + 1).join("/");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Verifica se é arquivo de recurso web (JSP, HTML, etc)
|
|
122
|
+
*/
|
|
123
|
+
static isWebResource(filePath: string): boolean {
|
|
124
|
+
return /\.(jsp|html|htm|css|js|xml|properties|json|png|jpg|jpeg|gif|svg|ico)$/i.test(filePath);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Verifica se é arquivo Java
|
|
129
|
+
*/
|
|
130
|
+
static isJavaFile(filePath: string): boolean {
|
|
131
|
+
return /\.java$/i.test(filePath);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Verifica se é arquivo de build/configuração
|
|
136
|
+
*/
|
|
137
|
+
static isBuildConfig(filePath: string): boolean {
|
|
138
|
+
return /(pom\.xml|build\.gradle|build\.gradle\.kts|settings\.gradle)$/i.test(filePath);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Combina múltiplos paths de forma segura
|
|
143
|
+
*/
|
|
144
|
+
static combinePaths(...parts: string[]): string {
|
|
145
|
+
return path.join(...parts);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Obtém diretório do arquivo
|
|
150
|
+
*/
|
|
151
|
+
static getDir(filePath: string): string {
|
|
152
|
+
return path.dirname(filePath);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Obtém nome do arquivo
|
|
157
|
+
*/
|
|
158
|
+
static getFilename(filePath: string): string {
|
|
159
|
+
return path.basename(filePath);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Obtém extensão do arquivo
|
|
164
|
+
*/
|
|
165
|
+
static getExtension(filePath: string): string {
|
|
166
|
+
return path.extname(filePath).toLowerCase();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Remove extensão do arquivo
|
|
171
|
+
*/
|
|
172
|
+
static removeExtension(filePath: string): string {
|
|
173
|
+
const ext = path.extname(filePath);
|
|
174
|
+
return ext ? filePath.slice(0, -ext.length) : filePath;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Verifica se caminho está dentro de diretório ignorado
|
|
179
|
+
*/
|
|
180
|
+
static isIgnoredPath(filePath: string, ignoredDirs: string[] = []): boolean {
|
|
181
|
+
const normalized = this.normalizeSeparators(filePath);
|
|
182
|
+
const defaultIgnored = ["target", "build", "node_modules", ".git", ".xavva"];
|
|
183
|
+
const allIgnored = [...defaultIgnored, ...ignoredDirs];
|
|
184
|
+
|
|
185
|
+
return allIgnored.some(dir => {
|
|
186
|
+
// Match exato ou como diretório
|
|
187
|
+
const pattern = new RegExp(`(^|/)${dir}($|/)`);
|
|
188
|
+
return pattern.test(normalized);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Resolve caminho de contexto WAR
|
|
194
|
+
*/
|
|
195
|
+
static resolveContextPath(appName: string): string {
|
|
196
|
+
return appName.replace(/\.war$/i, "");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Exporta funções standalone para conveniência
|
|
201
|
+
export const {
|
|
202
|
+
normalizeSeparators,
|
|
203
|
+
resolveFromCwd,
|
|
204
|
+
relativeToProject,
|
|
205
|
+
isSourceFile,
|
|
206
|
+
extractPackageFromPath,
|
|
207
|
+
javaToClassPath,
|
|
208
|
+
sourceToClassesPath,
|
|
209
|
+
findWebappRoot,
|
|
210
|
+
getWebappRelativePath,
|
|
211
|
+
isWebResource,
|
|
212
|
+
isJavaFile,
|
|
213
|
+
isBuildConfig,
|
|
214
|
+
combinePaths,
|
|
215
|
+
getDir,
|
|
216
|
+
getFilename,
|
|
217
|
+
getExtension,
|
|
218
|
+
removeExtension,
|
|
219
|
+
isIgnoredPath,
|
|
220
|
+
resolveContextPath,
|
|
221
|
+
} = PathUtils;
|
package/src/utils/config.ts
CHANGED
|
@@ -38,6 +38,12 @@ export class ConfigManager {
|
|
|
38
38
|
yes: { type: "boolean", short: "y" },
|
|
39
39
|
war: { type: "boolean", short: "W" },
|
|
40
40
|
cache: { type: "boolean" },
|
|
41
|
+
from: { type: "string" },
|
|
42
|
+
to: { type: "string" },
|
|
43
|
+
backup: { type: "boolean" },
|
|
44
|
+
"dry-run": { type: "boolean" },
|
|
45
|
+
force: { type: "boolean" },
|
|
46
|
+
src: { type: "string" },
|
|
41
47
|
},
|
|
42
48
|
strict: false,
|
|
43
49
|
allowPositionals: true,
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser de código Java
|
|
3
|
+
* Centraliza análise de anotações e estrutura de arquivos .java
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ApiEndpoint, ApiParam } from "../../types/endpoint";
|
|
7
|
+
|
|
8
|
+
export interface JavaClassInfo {
|
|
9
|
+
className: string;
|
|
10
|
+
package?: string;
|
|
11
|
+
annotations: AnnotationInfo[];
|
|
12
|
+
methods: JavaMethodInfo[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AnnotationInfo {
|
|
16
|
+
name: string;
|
|
17
|
+
value?: string;
|
|
18
|
+
attributes: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface JavaMethodInfo {
|
|
22
|
+
name: string;
|
|
23
|
+
annotations: AnnotationInfo[];
|
|
24
|
+
parameters: JavaParameterInfo[];
|
|
25
|
+
returnType?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface JavaParameterInfo {
|
|
29
|
+
name: string;
|
|
30
|
+
type: string;
|
|
31
|
+
annotations: AnnotationInfo[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class JavaParser {
|
|
35
|
+
/**
|
|
36
|
+
* Extrai informações completas de uma classe Java
|
|
37
|
+
*/
|
|
38
|
+
static parseClass(content: string, fileName: string): JavaClassInfo {
|
|
39
|
+
const className = fileName.replace(/\.java$/i, "");
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
className,
|
|
43
|
+
package: this.extractPackage(content),
|
|
44
|
+
annotations: this.extractClassAnnotations(content),
|
|
45
|
+
methods: this.extractMethods(content),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extrai package declaration
|
|
51
|
+
*/
|
|
52
|
+
static extractPackage(content: string): string | undefined {
|
|
53
|
+
const match = content.match(/package\s+([\w.]+)\s*;/);
|
|
54
|
+
return match?.[1];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extrai imports
|
|
59
|
+
*/
|
|
60
|
+
static extractImports(content: string): string[] {
|
|
61
|
+
const imports: string[] = [];
|
|
62
|
+
const regex = /import\s+([\w.*]+)\s*;/g;
|
|
63
|
+
let match;
|
|
64
|
+
while ((match = regex.exec(content)) !== null) {
|
|
65
|
+
imports.push(match[1]);
|
|
66
|
+
}
|
|
67
|
+
return imports;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extrai anotações de classe
|
|
72
|
+
*/
|
|
73
|
+
static extractClassAnnotations(content: string): AnnotationInfo[] {
|
|
74
|
+
// Procura anotações antes da declaração da classe
|
|
75
|
+
const classMatch = content.match(/(@[\w\s(,="{}[\]]+)\s+(?:public\s+)?(?:abstract\s+)?class\s+\w+/);
|
|
76
|
+
if (!classMatch) return [];
|
|
77
|
+
|
|
78
|
+
return this.parseAnnotations(classMatch[1]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extrai métodos da classe
|
|
83
|
+
*/
|
|
84
|
+
static extractMethods(content: string): JavaMethodInfo[] {
|
|
85
|
+
const methods: JavaMethodInfo[] = [];
|
|
86
|
+
|
|
87
|
+
// Regex melhorado para encontrar métodos
|
|
88
|
+
// Procura por padrões como: @Anotacao public Tipo metodo(params)
|
|
89
|
+
// Também captura métodos sem anotações
|
|
90
|
+
const methodPattern = /((?:@[\w]+(?:\s*\([^)]*\))?\s*)*)\s*(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)?(?:synchronized\s+)?([\w<>[\],\s.?]+)\s+(\w+)\s*\(([^)]*)\)\s*(?:throws\s+[\w,\s]+)?\s*[{;]/g;
|
|
91
|
+
|
|
92
|
+
let match;
|
|
93
|
+
while ((match = methodPattern.exec(content)) !== null) {
|
|
94
|
+
const annotationsStr = match[1] || "";
|
|
95
|
+
const returnType = match[2].trim();
|
|
96
|
+
const name = match[3];
|
|
97
|
+
const paramsStr = match[4];
|
|
98
|
+
|
|
99
|
+
// Pula métodos que parecem ser construtores (tipo == nome da classe)
|
|
100
|
+
// ou que não têm tipo de retorno válido
|
|
101
|
+
if (!returnType || returnType === name) continue;
|
|
102
|
+
|
|
103
|
+
methods.push({
|
|
104
|
+
name,
|
|
105
|
+
annotations: this.parseAnnotations(annotationsStr),
|
|
106
|
+
parameters: this.parseParameters(paramsStr),
|
|
107
|
+
returnType,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return methods;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parse de string de anotações
|
|
116
|
+
*/
|
|
117
|
+
static parseAnnotations(annotationsStr: string): AnnotationInfo[] {
|
|
118
|
+
const annotations: AnnotationInfo[] = [];
|
|
119
|
+
|
|
120
|
+
// Regex para anotações: @Nome ou @Nome(atributos)
|
|
121
|
+
const regex = /@(\w+)(?:\s*\(\s*([^)]*)\s*\))?/g;
|
|
122
|
+
|
|
123
|
+
let match;
|
|
124
|
+
while ((match = regex.exec(annotationsStr)) !== null) {
|
|
125
|
+
const name = match[1];
|
|
126
|
+
const attrsStr = match[2] || "";
|
|
127
|
+
|
|
128
|
+
const { value, attributes } = this.parseAnnotationAttributes(attrsStr);
|
|
129
|
+
|
|
130
|
+
annotations.push({ name, value, attributes });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return annotations;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parse de atributos de anotação
|
|
138
|
+
*/
|
|
139
|
+
private static parseAnnotationAttributes(attrsStr: string): { value?: string; attributes: Record<string, string> } {
|
|
140
|
+
const attributes: Record<string, string> = {};
|
|
141
|
+
let value: string | undefined;
|
|
142
|
+
|
|
143
|
+
if (!attrsStr.trim()) {
|
|
144
|
+
return { value, attributes };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Verifica se é valor simples (string literal ou array)
|
|
148
|
+
const simpleValueMatch = attrsStr.match(/^\s*["']([^"']+)["']\s*$/);
|
|
149
|
+
if (simpleValueMatch) {
|
|
150
|
+
value = simpleValueMatch[1];
|
|
151
|
+
return { value, attributes };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Parse de atributos chave=valor
|
|
155
|
+
// Pega tudo entre aspas após um sinal de igual
|
|
156
|
+
const attrRegex = /(\w+)\s*=\s*["']([^"']*?)["']/g;
|
|
157
|
+
let attrMatch;
|
|
158
|
+
while ((attrMatch = attrRegex.exec(attrsStr)) !== null) {
|
|
159
|
+
const key = attrMatch[1];
|
|
160
|
+
const val = attrMatch[2];
|
|
161
|
+
|
|
162
|
+
if (key === "value") {
|
|
163
|
+
value = val;
|
|
164
|
+
} else {
|
|
165
|
+
attributes[key] = val;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Se não encontrou atributos com regex, tenta extrair valor como string simples
|
|
170
|
+
if (Object.keys(attributes).length === 0 && !value) {
|
|
171
|
+
const simpleMatch = attrsStr.match(/["']([^"']+)["']/);
|
|
172
|
+
if (simpleMatch) {
|
|
173
|
+
value = simpleMatch[1];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { value, attributes };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Parse de parâmetros de método
|
|
182
|
+
*/
|
|
183
|
+
static parseParameters(paramsStr: string): JavaParameterInfo[] {
|
|
184
|
+
const parameters: JavaParameterInfo[] = [];
|
|
185
|
+
|
|
186
|
+
if (!paramsStr.trim()) {
|
|
187
|
+
return parameters;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Divide por vírgula, mas cuida com generics
|
|
191
|
+
const params = this.splitParams(paramsStr);
|
|
192
|
+
|
|
193
|
+
for (const param of params) {
|
|
194
|
+
const trimmed = param.trim();
|
|
195
|
+
if (!trimmed) continue;
|
|
196
|
+
|
|
197
|
+
// Extrai anotações do parâmetro
|
|
198
|
+
const annotations: AnnotationInfo[] = [];
|
|
199
|
+
let paramWithoutAnnotations = trimmed;
|
|
200
|
+
|
|
201
|
+
const annoRegex = /@(\w+)\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
202
|
+
let annoMatch;
|
|
203
|
+
while ((annoMatch = annoRegex.exec(trimmed)) !== null) {
|
|
204
|
+
annotations.push({
|
|
205
|
+
name: annoMatch[1],
|
|
206
|
+
value: annoMatch[2],
|
|
207
|
+
attributes: {},
|
|
208
|
+
});
|
|
209
|
+
paramWithoutAnnotations = paramWithoutAnnotations.replace(annoMatch[0], "").trim();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Tipo e nome
|
|
213
|
+
const parts = paramWithoutAnnotations.split(/\s+/);
|
|
214
|
+
if (parts.length >= 2) {
|
|
215
|
+
parameters.push({
|
|
216
|
+
type: parts.slice(0, -1).join(" "),
|
|
217
|
+
name: parts[parts.length - 1],
|
|
218
|
+
annotations,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return parameters;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Divide string de parâmetros respeitando generics
|
|
228
|
+
*/
|
|
229
|
+
private static splitParams(paramsStr: string): string[] {
|
|
230
|
+
const result: string[] = [];
|
|
231
|
+
let current = "";
|
|
232
|
+
let depth = 0;
|
|
233
|
+
|
|
234
|
+
for (const char of paramsStr) {
|
|
235
|
+
if (char === "<") {
|
|
236
|
+
depth++;
|
|
237
|
+
current += char;
|
|
238
|
+
} else if (char === ">") {
|
|
239
|
+
depth--;
|
|
240
|
+
current += char;
|
|
241
|
+
} else if (char === "," && depth === 0) {
|
|
242
|
+
result.push(current);
|
|
243
|
+
current = "";
|
|
244
|
+
} else {
|
|
245
|
+
current += char;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (current.trim()) {
|
|
250
|
+
result.push(current);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Extrai endpoints REST de um arquivo Java
|
|
258
|
+
* Suporta Spring MVC, JAX-RS, Jersey
|
|
259
|
+
*/
|
|
260
|
+
static extractEndpoints(content: string, fileName: string, contextPath: string = ""): ApiEndpoint[] {
|
|
261
|
+
const classInfo = this.parseClass(content, fileName);
|
|
262
|
+
const endpoints: ApiEndpoint[] = [];
|
|
263
|
+
|
|
264
|
+
// Encontra path base da classe
|
|
265
|
+
const basePath = this.extractBasePath(classInfo.annotations);
|
|
266
|
+
|
|
267
|
+
for (const method of classInfo.methods) {
|
|
268
|
+
const methodPath = this.extractMethodPath(method.annotations);
|
|
269
|
+
const httpMethod = this.inferHttpMethod(method.annotations);
|
|
270
|
+
|
|
271
|
+
if (!methodPath && httpMethod === "ALL") continue;
|
|
272
|
+
|
|
273
|
+
const fullPath = this.combinePaths(contextPath, basePath, methodPath);
|
|
274
|
+
|
|
275
|
+
endpoints.push({
|
|
276
|
+
method: httpMethod,
|
|
277
|
+
path: methodPath || "/",
|
|
278
|
+
fullPath,
|
|
279
|
+
className: classInfo.className,
|
|
280
|
+
methodName: method.name,
|
|
281
|
+
parameters: this.convertParams(method.parameters),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return endpoints;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Extrai path base de anotações de classe
|
|
290
|
+
*/
|
|
291
|
+
private static extractBasePath(annotations: AnnotationInfo[]): string {
|
|
292
|
+
// Spring: @RequestMapping("/path") ou @Path("/path") (JAX-RS)
|
|
293
|
+
const mapping = annotations.find(a =>
|
|
294
|
+
a.name === "RequestMapping" || a.name === "Path"
|
|
295
|
+
);
|
|
296
|
+
return mapping?.value || "";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Extrai path de método de anotações
|
|
301
|
+
*/
|
|
302
|
+
private static extractMethodPath(annotations: AnnotationInfo[]): string {
|
|
303
|
+
// Procura qualquer anotação que tenha valor de path
|
|
304
|
+
const pathAnnotations = [
|
|
305
|
+
"RequestMapping", "GetMapping", "PostMapping",
|
|
306
|
+
"PutMapping", "DeleteMapping", "PatchMapping",
|
|
307
|
+
"Path"
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
const mapping = annotations.find(a => pathAnnotations.includes(a.name));
|
|
311
|
+
return mapping?.value || "";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Infere método HTTP de anotações
|
|
316
|
+
*/
|
|
317
|
+
private static inferHttpMethod(annotations: AnnotationInfo[]): ApiEndpoint["method"] {
|
|
318
|
+
const annoNames = annotations.map(a => a.name);
|
|
319
|
+
|
|
320
|
+
if (annoNames.includes("GetMapping")) return "GET";
|
|
321
|
+
if (annoNames.includes("PostMapping")) return "POST";
|
|
322
|
+
if (annoNames.includes("PutMapping")) return "PUT";
|
|
323
|
+
if (annoNames.includes("DeleteMapping")) return "DELETE";
|
|
324
|
+
if (annoNames.includes("PatchMapping")) return "PATCH";
|
|
325
|
+
|
|
326
|
+
// JAX-RS
|
|
327
|
+
if (annoNames.includes("GET")) return "GET";
|
|
328
|
+
if (annoNames.includes("POST")) return "POST";
|
|
329
|
+
if (annoNames.includes("PUT")) return "PUT";
|
|
330
|
+
if (annoNames.includes("DELETE")) return "DELETE";
|
|
331
|
+
if (annoNames.includes("PATCH")) return "PATCH";
|
|
332
|
+
|
|
333
|
+
// RequestMapping genérico
|
|
334
|
+
if (annoNames.includes("RequestMapping")) {
|
|
335
|
+
const mapping = annotations.find(a => a.name === "RequestMapping");
|
|
336
|
+
const methodAttr = mapping?.attributes?.method;
|
|
337
|
+
if (methodAttr) {
|
|
338
|
+
const method = methodAttr.replace(/RequestMethod\./g, "").toUpperCase();
|
|
339
|
+
if (["GET", "POST", "PUT", "DELETE", "PATCH"].includes(method)) {
|
|
340
|
+
return method as ApiEndpoint["method"];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return "ALL";
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return "ALL";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Converte parâmetros Java para ApiParam
|
|
351
|
+
*/
|
|
352
|
+
private static convertParams(params: JavaParameterInfo[]): ApiParam[] {
|
|
353
|
+
return params.map(p => {
|
|
354
|
+
const source = this.inferParamSource(p.annotations);
|
|
355
|
+
const required = !p.annotations.some(a =>
|
|
356
|
+
a.name === "RequestParam" && a.attributes?.required === "false"
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
name: p.name,
|
|
361
|
+
type: p.type,
|
|
362
|
+
source,
|
|
363
|
+
required,
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Infere fonte do parâmetro (PATH, QUERY, BODY, HEADER)
|
|
370
|
+
*/
|
|
371
|
+
private static inferParamSource(annotations: AnnotationInfo[]): ApiParam["source"] {
|
|
372
|
+
const annoNames = annotations.map(a => a.name);
|
|
373
|
+
|
|
374
|
+
if (annoNames.includes("PathVariable") || annoNames.includes("PathParam")) {
|
|
375
|
+
return "PATH";
|
|
376
|
+
}
|
|
377
|
+
if (annoNames.includes("RequestParam") || annoNames.includes("QueryParam")) {
|
|
378
|
+
return "QUERY";
|
|
379
|
+
}
|
|
380
|
+
if (annoNames.includes("RequestBody")) {
|
|
381
|
+
return "BODY";
|
|
382
|
+
}
|
|
383
|
+
if (annoNames.includes("RequestHeader") || annoNames.includes("HeaderParam")) {
|
|
384
|
+
return "HEADER";
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return "QUERY"; // Default
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Combina múltiplos paths
|
|
392
|
+
*/
|
|
393
|
+
private static combinePaths(...parts: string[]): string {
|
|
394
|
+
return parts
|
|
395
|
+
.map(p => p.trim())
|
|
396
|
+
.map(p => (p.startsWith("/") ? p : "/" + p))
|
|
397
|
+
.map(p => (p.endsWith("/") && p.length > 1 ? p.slice(0, -1) : p))
|
|
398
|
+
.filter(p => p && p !== "/")
|
|
399
|
+
.join("") || "/";
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Exporta funções standalone
|
|
404
|
+
export const {
|
|
405
|
+
parseClass,
|
|
406
|
+
extractPackage,
|
|
407
|
+
extractImports,
|
|
408
|
+
extractClassAnnotations,
|
|
409
|
+
extractMethods,
|
|
410
|
+
parseAnnotations,
|
|
411
|
+
parseParameters,
|
|
412
|
+
extractEndpoints,
|
|
413
|
+
} = JavaParser;
|