@g1cloud/api-gen 1.0.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/.claude/settings.local.json +22 -0
- package/CLAUDE.md +63 -0
- package/README.md +379 -0
- package/dist/analyzer/controllerAnalyzer.d.ts +20 -0
- package/dist/analyzer/controllerAnalyzer.d.ts.map +1 -0
- package/dist/analyzer/controllerAnalyzer.js +101 -0
- package/dist/analyzer/controllerAnalyzer.js.map +1 -0
- package/dist/analyzer/parameterAnalyzer.d.ts +19 -0
- package/dist/analyzer/parameterAnalyzer.d.ts.map +1 -0
- package/dist/analyzer/parameterAnalyzer.js +207 -0
- package/dist/analyzer/parameterAnalyzer.js.map +1 -0
- package/dist/analyzer/responseAnalyzer.d.ts +12 -0
- package/dist/analyzer/responseAnalyzer.d.ts.map +1 -0
- package/dist/analyzer/responseAnalyzer.js +116 -0
- package/dist/analyzer/responseAnalyzer.js.map +1 -0
- package/dist/analyzer/schemaGenerator.d.ts +6 -0
- package/dist/analyzer/schemaGenerator.d.ts.map +1 -0
- package/dist/analyzer/schemaGenerator.js +347 -0
- package/dist/analyzer/schemaGenerator.js.map +1 -0
- package/dist/analyzer/securityAnalyzer.d.ts +6 -0
- package/dist/analyzer/securityAnalyzer.d.ts.map +1 -0
- package/dist/analyzer/securityAnalyzer.js +177 -0
- package/dist/analyzer/securityAnalyzer.js.map +1 -0
- package/dist/generator/openapiGenerator.d.ts +14 -0
- package/dist/generator/openapiGenerator.d.ts.map +1 -0
- package/dist/generator/openapiGenerator.js +340 -0
- package/dist/generator/openapiGenerator.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +218 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +61 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +199 -0
- package/dist/lib.js.map +1 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +257 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/mcp-server.mjs +45586 -0
- package/dist/parser/astAnalyzer.d.ts +87 -0
- package/dist/parser/astAnalyzer.d.ts.map +1 -0
- package/dist/parser/astAnalyzer.js +321 -0
- package/dist/parser/astAnalyzer.js.map +1 -0
- package/dist/parser/javaParser.d.ts +10 -0
- package/dist/parser/javaParser.d.ts.map +1 -0
- package/dist/parser/javaParser.js +805 -0
- package/dist/parser/javaParser.js.map +1 -0
- package/dist/types/index.d.ts +217 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/examples/CreateUserRequest.java +80 -0
- package/examples/DepartmentDTO.java +45 -0
- package/examples/Filter.java +39 -0
- package/examples/PaginatedList.java +71 -0
- package/examples/ProductController.java +136 -0
- package/examples/ProductDTO.java +129 -0
- package/examples/RoleDTO.java +47 -0
- package/examples/SearchParam.java +55 -0
- package/examples/Sort.java +70 -0
- package/examples/UpdateUserRequest.java +74 -0
- package/examples/UserController.java +98 -0
- package/examples/UserDTO.java +119 -0
- package/package.json +51 -0
- package/prompt/01_Initial.md +358 -0
- package/prompt/02_/354/266/224/352/260/200.md +31 -0
- package/src/analyzer/controllerAnalyzer.ts +125 -0
- package/src/analyzer/parameterAnalyzer.ts +259 -0
- package/src/analyzer/responseAnalyzer.ts +142 -0
- package/src/analyzer/schemaGenerator.ts +412 -0
- package/src/analyzer/securityAnalyzer.ts +200 -0
- package/src/generator/openapiGenerator.ts +378 -0
- package/src/index.ts +212 -0
- package/src/lib.ts +240 -0
- package/src/mcp-server.ts +310 -0
- package/src/parser/astAnalyzer.ts +373 -0
- package/src/parser/javaParser.ts +901 -0
- package/src/types/index.ts +238 -0
- package/test-boolean.yaml +607 -0
- package/test-filter.yaml +576 -0
- package/test-inner.ts +59 -0
- package/test-output.yaml +650 -0
- package/test-paginated.yaml +585 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +30 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JavaClass,
|
|
3
|
+
JavaField,
|
|
4
|
+
JavaAnnotation,
|
|
5
|
+
SchemaInfo,
|
|
6
|
+
ProcessingContext,
|
|
7
|
+
} from '../types';
|
|
8
|
+
import { ASTAnalyzer } from '../parser/astAnalyzer';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate OpenAPI schemas for all referenced types
|
|
12
|
+
*/
|
|
13
|
+
export function generateSchemas(context: ProcessingContext): Map<string, SchemaInfo> {
|
|
14
|
+
const schemas = new Map<string, SchemaInfo>(context.dtoSchemas);
|
|
15
|
+
|
|
16
|
+
// Add standard schemas for SearchParam, Filter, Sort
|
|
17
|
+
addStandardSchemas(schemas);
|
|
18
|
+
|
|
19
|
+
// Process all referenced types
|
|
20
|
+
const processedTypes = new Set<string>();
|
|
21
|
+
const typesToProcess = [...context.referencedTypes];
|
|
22
|
+
|
|
23
|
+
while (typesToProcess.length > 0) {
|
|
24
|
+
const typeName = typesToProcess.pop()!;
|
|
25
|
+
|
|
26
|
+
if (processedTypes.has(typeName)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
processedTypes.add(typeName);
|
|
31
|
+
|
|
32
|
+
// Skip primitive types
|
|
33
|
+
if (ASTAnalyzer.isPrimitiveOrWrapper(typeName)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Skip if already in schemas
|
|
38
|
+
if (schemas.has(typeName)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Find the Java class for this type
|
|
43
|
+
const javaClass = context.javaClasses.get(typeName);
|
|
44
|
+
|
|
45
|
+
if (javaClass) {
|
|
46
|
+
// Handle enum types
|
|
47
|
+
if (javaClass.isEnum && javaClass.enumValues) {
|
|
48
|
+
schemas.set(typeName, {
|
|
49
|
+
type: 'string',
|
|
50
|
+
enum: javaClass.enumValues,
|
|
51
|
+
description: javaClass.javadoc,
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
const { schema, nestedTypes } = generateSchemaForClass(javaClass, context);
|
|
55
|
+
schemas.set(typeName, schema);
|
|
56
|
+
|
|
57
|
+
// Add nested types to process
|
|
58
|
+
for (const nestedType of nestedTypes) {
|
|
59
|
+
if (!processedTypes.has(nestedType)) {
|
|
60
|
+
typesToProcess.push(nestedType);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
// Type not found, create a generic object schema
|
|
66
|
+
schemas.set(typeName, {
|
|
67
|
+
type: 'object',
|
|
68
|
+
description: `Schema for ${typeName}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return schemas;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Add standard schemas for SearchParam, Filter, Sort
|
|
78
|
+
*/
|
|
79
|
+
function addStandardSchemas(schemas: Map<string, SchemaInfo>): void {
|
|
80
|
+
// Filter schema
|
|
81
|
+
if (!schemas.has('Filter')) {
|
|
82
|
+
schemas.set('Filter', {
|
|
83
|
+
type: 'object',
|
|
84
|
+
description: 'Filter conditions',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Sort schema
|
|
89
|
+
if (!schemas.has('Sort')) {
|
|
90
|
+
schemas.set('Sort', {
|
|
91
|
+
type: 'object',
|
|
92
|
+
description: 'Sort conditions',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// SearchParam schema (for reference)
|
|
97
|
+
if (!schemas.has('SearchParam')) {
|
|
98
|
+
schemas.set('SearchParam', {
|
|
99
|
+
type: 'object',
|
|
100
|
+
description: 'Search parameters for pagination and filtering',
|
|
101
|
+
properties: {
|
|
102
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
103
|
+
limit: { type: 'integer', description: 'Pagination limit' },
|
|
104
|
+
filter: { $ref: '#/components/schemas/Filter' },
|
|
105
|
+
sort: { $ref: '#/components/schemas/Sort' },
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// MultiLangString schema - Map<String, String>
|
|
111
|
+
if (!schemas.has('MultiLangString')) {
|
|
112
|
+
schemas.set('MultiLangString', {
|
|
113
|
+
type: 'object',
|
|
114
|
+
description: 'Multi-language string (Map<String, String>)',
|
|
115
|
+
additionalProperties: { type: 'string' },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// StoredFile schema
|
|
120
|
+
if (!schemas.has('StoredFile')) {
|
|
121
|
+
schemas.set('StoredFile', {
|
|
122
|
+
type: 'object',
|
|
123
|
+
description: 'Stored file information',
|
|
124
|
+
properties: {
|
|
125
|
+
fileUrl: { type: 'string', description: 'File URL' },
|
|
126
|
+
mediaType: { type: 'MediaType', enum: ['Image', 'Vide', 'Youtube', 'Unknown'], description: 'Media type' },
|
|
127
|
+
thumbnailUrl: { type: 'string', description: 'Thumbnail URL' },
|
|
128
|
+
fileName: { type: 'string', description: 'Original file name' },
|
|
129
|
+
altText: { type: 'string', description: 'Alternative text for the file' },
|
|
130
|
+
width: { type: 'integer', description: 'Width of the media' },
|
|
131
|
+
height: { type: 'integer', description: 'Height of the media' },
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// MultiLangStoredFile schema - Map<String, StoredFile>
|
|
137
|
+
if (!schemas.has('MultiLangStoredFile')) {
|
|
138
|
+
schemas.set('MultiLangStoredFile', {
|
|
139
|
+
type: 'object',
|
|
140
|
+
description: 'Multi-language stored file (Map<String, StoredFile>)',
|
|
141
|
+
additionalProperties: { $ref: '#/components/schemas/StoredFile' },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Generate schema for a Java class, including inherited fields from superclass
|
|
148
|
+
*/
|
|
149
|
+
function generateSchemaForClass(
|
|
150
|
+
javaClass: JavaClass,
|
|
151
|
+
context: ProcessingContext
|
|
152
|
+
): { schema: SchemaInfo; nestedTypes: string[] } {
|
|
153
|
+
const properties: Record<string, SchemaInfo> = {};
|
|
154
|
+
const required: string[] = [];
|
|
155
|
+
const nestedTypes: string[] = [];
|
|
156
|
+
|
|
157
|
+
// Collect all fields including inherited ones
|
|
158
|
+
const allFields = collectAllFields(javaClass, context);
|
|
159
|
+
|
|
160
|
+
for (const field of allFields) {
|
|
161
|
+
const { propertySchema, isRequired, referencedType } = generatePropertySchema(field);
|
|
162
|
+
|
|
163
|
+
properties[field.name] = propertySchema;
|
|
164
|
+
|
|
165
|
+
if (isRequired) {
|
|
166
|
+
required.push(field.name);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (referencedType) {
|
|
170
|
+
nestedTypes.push(referencedType);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const schema: SchemaInfo = {
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
if (required.length > 0) {
|
|
180
|
+
schema.required = required;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { schema, nestedTypes };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Collect all fields from a class and its superclasses
|
|
188
|
+
*/
|
|
189
|
+
function collectAllFields(javaClass: JavaClass, context: ProcessingContext): JavaField[] {
|
|
190
|
+
const fields: JavaField[] = [];
|
|
191
|
+
const visitedClasses = new Set<string>();
|
|
192
|
+
|
|
193
|
+
let currentClass: JavaClass | undefined = javaClass;
|
|
194
|
+
|
|
195
|
+
while (currentClass && !visitedClasses.has(currentClass.name)) {
|
|
196
|
+
visitedClasses.add(currentClass.name);
|
|
197
|
+
|
|
198
|
+
// Add fields from current class (prepend to maintain inheritance order: parent fields first)
|
|
199
|
+
fields.unshift(...currentClass.fields);
|
|
200
|
+
|
|
201
|
+
// Move to superclass
|
|
202
|
+
if (currentClass.superClass) {
|
|
203
|
+
currentClass = context.javaClasses.get(currentClass.superClass);
|
|
204
|
+
} else {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return fields;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Generate schema for a field
|
|
214
|
+
*/
|
|
215
|
+
function generatePropertySchema(field: JavaField): {
|
|
216
|
+
propertySchema: SchemaInfo;
|
|
217
|
+
isRequired: boolean;
|
|
218
|
+
referencedType?: string;
|
|
219
|
+
} {
|
|
220
|
+
let isRequired = false;
|
|
221
|
+
let referencedType: string | undefined;
|
|
222
|
+
|
|
223
|
+
// Check for validation annotations
|
|
224
|
+
const constraints = extractValidationConstraints(field.annotations);
|
|
225
|
+
isRequired = constraints.required;
|
|
226
|
+
|
|
227
|
+
// Generate base schema based on type
|
|
228
|
+
let propertySchema: SchemaInfo;
|
|
229
|
+
|
|
230
|
+
if (ASTAnalyzer.isPrimitiveOrWrapper(field.type)) {
|
|
231
|
+
propertySchema = ASTAnalyzer.javaTypeToOpenAPI(field.type);
|
|
232
|
+
} else if (field.type.endsWith('[]')) {
|
|
233
|
+
const elementType = field.type.slice(0, -2);
|
|
234
|
+
if (ASTAnalyzer.isPrimitiveOrWrapper(elementType)) {
|
|
235
|
+
propertySchema = {
|
|
236
|
+
type: 'array',
|
|
237
|
+
items: ASTAnalyzer.javaTypeToOpenAPI(elementType),
|
|
238
|
+
};
|
|
239
|
+
} else {
|
|
240
|
+
referencedType = elementType;
|
|
241
|
+
propertySchema = {
|
|
242
|
+
type: 'array',
|
|
243
|
+
items: { $ref: `#/components/schemas/${elementType}` },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
} else if (ASTAnalyzer.isCollectionType(field.type)) {
|
|
247
|
+
const itemType = field.genericType ? extractMainType(field.genericType) : 'Object';
|
|
248
|
+
if (ASTAnalyzer.isPrimitiveOrWrapper(itemType)) {
|
|
249
|
+
propertySchema = {
|
|
250
|
+
type: 'array',
|
|
251
|
+
items: ASTAnalyzer.javaTypeToOpenAPI(itemType),
|
|
252
|
+
};
|
|
253
|
+
} else {
|
|
254
|
+
referencedType = itemType;
|
|
255
|
+
propertySchema = {
|
|
256
|
+
type: 'array',
|
|
257
|
+
items: { $ref: `#/components/schemas/${itemType}` },
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
} else if (ASTAnalyzer.isMapType(field.type)) {
|
|
261
|
+
propertySchema = {
|
|
262
|
+
type: 'object',
|
|
263
|
+
additionalProperties: true,
|
|
264
|
+
};
|
|
265
|
+
} else if (isEnumType(field)) {
|
|
266
|
+
propertySchema = {
|
|
267
|
+
type: 'string',
|
|
268
|
+
enum: constraints.enumValues,
|
|
269
|
+
};
|
|
270
|
+
} else {
|
|
271
|
+
// Reference type
|
|
272
|
+
referencedType = field.type.split('.').pop() || field.type;
|
|
273
|
+
propertySchema = { $ref: `#/components/schemas/${referencedType}` };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Apply constraints to schema
|
|
277
|
+
if (constraints.minLength !== undefined && propertySchema.type === 'string') {
|
|
278
|
+
propertySchema.minLength = constraints.minLength;
|
|
279
|
+
}
|
|
280
|
+
if (constraints.maxLength !== undefined && propertySchema.type === 'string') {
|
|
281
|
+
propertySchema.maxLength = constraints.maxLength;
|
|
282
|
+
}
|
|
283
|
+
if (constraints.minimum !== undefined && (propertySchema.type === 'integer' || propertySchema.type === 'number')) {
|
|
284
|
+
propertySchema.minimum = constraints.minimum;
|
|
285
|
+
}
|
|
286
|
+
if (constraints.maximum !== undefined && (propertySchema.type === 'integer' || propertySchema.type === 'number')) {
|
|
287
|
+
propertySchema.maximum = constraints.maximum;
|
|
288
|
+
}
|
|
289
|
+
if (constraints.pattern) {
|
|
290
|
+
propertySchema.pattern = constraints.pattern;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Apply description: annotation description takes priority, then javadoc
|
|
294
|
+
if (constraints.description) {
|
|
295
|
+
propertySchema.description = constraints.description;
|
|
296
|
+
} else if (field.javadoc) {
|
|
297
|
+
propertySchema.description = field.javadoc;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { propertySchema, isRequired, referencedType };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
interface ValidationConstraints {
|
|
304
|
+
required: boolean;
|
|
305
|
+
minLength?: number;
|
|
306
|
+
maxLength?: number;
|
|
307
|
+
minimum?: number;
|
|
308
|
+
maximum?: number;
|
|
309
|
+
pattern?: string;
|
|
310
|
+
description?: string;
|
|
311
|
+
enumValues?: string[];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Extract validation constraints from field annotations
|
|
316
|
+
*/
|
|
317
|
+
function extractValidationConstraints(annotations: JavaAnnotation[]): ValidationConstraints {
|
|
318
|
+
const constraints: ValidationConstraints = { required: false };
|
|
319
|
+
|
|
320
|
+
for (const annotation of annotations) {
|
|
321
|
+
switch (annotation.name) {
|
|
322
|
+
case 'NotNull':
|
|
323
|
+
case 'NotEmpty':
|
|
324
|
+
case 'NotBlank':
|
|
325
|
+
constraints.required = true;
|
|
326
|
+
break;
|
|
327
|
+
|
|
328
|
+
case 'Size':
|
|
329
|
+
if (annotation.values['min']) {
|
|
330
|
+
constraints.minLength = parseInt(String(annotation.values['min']), 10);
|
|
331
|
+
}
|
|
332
|
+
if (annotation.values['max']) {
|
|
333
|
+
constraints.maxLength = parseInt(String(annotation.values['max']), 10);
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
336
|
+
|
|
337
|
+
case 'Min':
|
|
338
|
+
if (annotation.values['value']) {
|
|
339
|
+
constraints.minimum = parseInt(String(annotation.values['value']), 10);
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
|
|
343
|
+
case 'Max':
|
|
344
|
+
if (annotation.values['value']) {
|
|
345
|
+
constraints.maximum = parseInt(String(annotation.values['value']), 10);
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
|
|
349
|
+
case 'Pattern':
|
|
350
|
+
if (annotation.values['regexp']) {
|
|
351
|
+
constraints.pattern = String(annotation.values['regexp']);
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
|
|
355
|
+
case 'Email':
|
|
356
|
+
constraints.pattern = '^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$';
|
|
357
|
+
break;
|
|
358
|
+
|
|
359
|
+
case 'Length':
|
|
360
|
+
if (annotation.values['min']) {
|
|
361
|
+
constraints.minLength = parseInt(String(annotation.values['min']), 10);
|
|
362
|
+
}
|
|
363
|
+
if (annotation.values['max']) {
|
|
364
|
+
constraints.maxLength = parseInt(String(annotation.values['max']), 10);
|
|
365
|
+
}
|
|
366
|
+
break;
|
|
367
|
+
|
|
368
|
+
case 'Range':
|
|
369
|
+
if (annotation.values['min']) {
|
|
370
|
+
constraints.minimum = parseInt(String(annotation.values['min']), 10);
|
|
371
|
+
}
|
|
372
|
+
if (annotation.values['max']) {
|
|
373
|
+
constraints.maximum = parseInt(String(annotation.values['max']), 10);
|
|
374
|
+
}
|
|
375
|
+
break;
|
|
376
|
+
|
|
377
|
+
case 'ApiModelProperty':
|
|
378
|
+
case 'Schema':
|
|
379
|
+
if (annotation.values['description']) {
|
|
380
|
+
constraints.description = String(annotation.values['description']);
|
|
381
|
+
}
|
|
382
|
+
if (annotation.values['required'] === 'true' || annotation.values['required'] === true) {
|
|
383
|
+
constraints.required = true;
|
|
384
|
+
}
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return constraints;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Check if field is an enum type
|
|
394
|
+
*/
|
|
395
|
+
function isEnumType(field: JavaField): boolean {
|
|
396
|
+
// This is a simplified check - in practice, you'd need to analyze the type
|
|
397
|
+
const enumAnnotations = field.annotations.filter(
|
|
398
|
+
(a) => a.name === 'Enumerated' || (a.name === 'Schema' && a.values['allowableValues'])
|
|
399
|
+
);
|
|
400
|
+
return enumAnnotations.length > 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Extract main type from generic type string
|
|
405
|
+
*/
|
|
406
|
+
function extractMainType(genericType: string): string {
|
|
407
|
+
const match = genericType.match(/^([^<]+)/);
|
|
408
|
+
if (match) {
|
|
409
|
+
return match[1].trim();
|
|
410
|
+
}
|
|
411
|
+
return genericType;
|
|
412
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { JavaMethod, JavaAnnotation, SecurityInfo } from '../types';
|
|
2
|
+
import { ASTAnalyzer } from '../parser/astAnalyzer';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Analyze method security annotations and extract roles/authorities
|
|
6
|
+
*/
|
|
7
|
+
export function analyzeMethodSecurity(method: JavaMethod): SecurityInfo {
|
|
8
|
+
const securityInfo: SecurityInfo = {
|
|
9
|
+
roles: [],
|
|
10
|
+
authorities: [],
|
|
11
|
+
hasComplexExpression: false,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const securityAnnotations = ASTAnalyzer.getSecurityAnnotations(method);
|
|
15
|
+
|
|
16
|
+
for (const annotation of securityAnnotations) {
|
|
17
|
+
switch (annotation.name) {
|
|
18
|
+
case 'PreAuthorize':
|
|
19
|
+
case 'PostAuthorize':
|
|
20
|
+
analyzeSpELExpression(annotation, securityInfo);
|
|
21
|
+
break;
|
|
22
|
+
case 'Secured':
|
|
23
|
+
analyzeSecuredAnnotation(annotation, securityInfo);
|
|
24
|
+
break;
|
|
25
|
+
case 'RolesAllowed':
|
|
26
|
+
analyzeRolesAllowedAnnotation(annotation, securityInfo);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Remove duplicates
|
|
32
|
+
securityInfo.roles = [...new Set(securityInfo.roles)];
|
|
33
|
+
securityInfo.authorities = [...new Set(securityInfo.authorities)];
|
|
34
|
+
|
|
35
|
+
return securityInfo;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Analyze @PreAuthorize / @PostAuthorize SpEL expression
|
|
40
|
+
*/
|
|
41
|
+
function analyzeSpELExpression(annotation: JavaAnnotation, securityInfo: SecurityInfo): void {
|
|
42
|
+
const expression = getAnnotationValue(annotation);
|
|
43
|
+
if (!expression) return;
|
|
44
|
+
|
|
45
|
+
// Extract hasRole() calls
|
|
46
|
+
const hasRoleMatches = expression.matchAll(/hasRole\s*\(\s*'([^']+)'\s*\)/g);
|
|
47
|
+
for (const match of hasRoleMatches) {
|
|
48
|
+
const role = normalizeRole(match[1]);
|
|
49
|
+
securityInfo.roles.push(role);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Extract hasAnyRole() calls
|
|
53
|
+
const hasAnyRoleMatches = expression.matchAll(/hasAnyRole\s*\(\s*([^)]+)\s*\)/g);
|
|
54
|
+
for (const match of hasAnyRoleMatches) {
|
|
55
|
+
const rolesString = match[1];
|
|
56
|
+
const roles = extractRolesFromString(rolesString);
|
|
57
|
+
securityInfo.roles.push(...roles.map(normalizeRole));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Extract hasAuthority() calls
|
|
61
|
+
const hasAuthorityMatches = expression.matchAll(/hasAuthority\s*\(\s*'([^']+)'\s*\)/g);
|
|
62
|
+
for (const match of hasAuthorityMatches) {
|
|
63
|
+
securityInfo.authorities.push(match[1]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Extract hasAnyAuthority() calls
|
|
67
|
+
const hasAnyAuthorityMatches = expression.matchAll(/hasAnyAuthority\s*\(\s*([^)]+)\s*\)/g);
|
|
68
|
+
for (const match of hasAnyAuthorityMatches) {
|
|
69
|
+
const authoritiesString = match[1];
|
|
70
|
+
const authorities = extractRolesFromString(authoritiesString);
|
|
71
|
+
securityInfo.authorities.push(...authorities);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check if the expression is complex (has more than just role/authority checks)
|
|
75
|
+
const isComplex = isComplexSpELExpression(expression);
|
|
76
|
+
if (isComplex) {
|
|
77
|
+
securityInfo.hasComplexExpression = true;
|
|
78
|
+
securityInfo.securityExpression = expression;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Analyze @Secured annotation
|
|
84
|
+
*/
|
|
85
|
+
function analyzeSecuredAnnotation(annotation: JavaAnnotation, securityInfo: SecurityInfo): void {
|
|
86
|
+
const value = annotation.values['value'];
|
|
87
|
+
if (!value) return;
|
|
88
|
+
|
|
89
|
+
const roles = Array.isArray(value) ? value : [value];
|
|
90
|
+
for (const role of roles) {
|
|
91
|
+
if (typeof role === 'string') {
|
|
92
|
+
securityInfo.roles.push(normalizeRole(role));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Analyze @RolesAllowed annotation
|
|
99
|
+
*/
|
|
100
|
+
function analyzeRolesAllowedAnnotation(annotation: JavaAnnotation, securityInfo: SecurityInfo): void {
|
|
101
|
+
const value = annotation.values['value'];
|
|
102
|
+
if (!value) return;
|
|
103
|
+
|
|
104
|
+
const roles = Array.isArray(value) ? value : [value];
|
|
105
|
+
for (const role of roles) {
|
|
106
|
+
if (typeof role === 'string') {
|
|
107
|
+
securityInfo.roles.push(normalizeRole(role));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get annotation value
|
|
114
|
+
*/
|
|
115
|
+
function getAnnotationValue(annotation: JavaAnnotation): string | undefined {
|
|
116
|
+
const value = annotation.values['value'];
|
|
117
|
+
if (value === undefined) return undefined;
|
|
118
|
+
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
return value[0];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return typeof value === 'string' ? value : String(value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Normalize role name to include ROLE_ prefix if not present
|
|
128
|
+
*/
|
|
129
|
+
function normalizeRole(role: string): string {
|
|
130
|
+
const trimmedRole = role.trim().replace(/^['"]|['"]$/g, '');
|
|
131
|
+
|
|
132
|
+
if (trimmedRole.startsWith('ROLE_')) {
|
|
133
|
+
return trimmedRole;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return `ROLE_${trimmedRole}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Extract roles from a comma-separated string with quotes
|
|
141
|
+
* e.g., "'USER', 'ADMIN'" -> ["USER", "ADMIN"]
|
|
142
|
+
*/
|
|
143
|
+
function extractRolesFromString(rolesString: string): string[] {
|
|
144
|
+
const roles: string[] = [];
|
|
145
|
+
const matches = rolesString.matchAll(/'([^']+)'/g);
|
|
146
|
+
|
|
147
|
+
for (const match of matches) {
|
|
148
|
+
roles.push(match[1]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return roles;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if a SpEL expression is complex
|
|
156
|
+
* (contains more than just hasRole/hasAnyRole/hasAuthority/hasAnyAuthority)
|
|
157
|
+
*/
|
|
158
|
+
function isComplexSpELExpression(expression: string): boolean {
|
|
159
|
+
// Remove all simple role/authority checks
|
|
160
|
+
let simplified = expression
|
|
161
|
+
.replace(/hasRole\s*\(\s*'[^']+'\s*\)/g, '')
|
|
162
|
+
.replace(/hasAnyRole\s*\(\s*[^)]+\s*\)/g, '')
|
|
163
|
+
.replace(/hasAuthority\s*\(\s*'[^']+'\s*\)/g, '')
|
|
164
|
+
.replace(/hasAnyAuthority\s*\(\s*[^)]+\s*\)/g, '')
|
|
165
|
+
.replace(/\s+/g, ' ')
|
|
166
|
+
.trim();
|
|
167
|
+
|
|
168
|
+
// Remove boolean operators that might remain
|
|
169
|
+
simplified = simplified
|
|
170
|
+
.replace(/^\s*and\s*/gi, '')
|
|
171
|
+
.replace(/\s*and\s*$/gi, '')
|
|
172
|
+
.replace(/^\s*or\s*/gi, '')
|
|
173
|
+
.replace(/\s*or\s*$/gi, '')
|
|
174
|
+
.replace(/^\s*&&\s*/g, '')
|
|
175
|
+
.replace(/\s*&&\s*$/g, '')
|
|
176
|
+
.replace(/^\s*\|\|\s*/g, '')
|
|
177
|
+
.replace(/\s*\|\|\s*$/g, '')
|
|
178
|
+
.replace(/^\s*\(\s*\)\s*$/g, '')
|
|
179
|
+
.trim();
|
|
180
|
+
|
|
181
|
+
// If anything remains (other than parentheses and whitespace), it's complex
|
|
182
|
+
const hasOtherContent = simplified.replace(/[()]/g, '').trim().length > 0;
|
|
183
|
+
|
|
184
|
+
// Also check for specific complex patterns
|
|
185
|
+
const complexPatterns = [
|
|
186
|
+
/#\w+/, // Variable references like #user, #id
|
|
187
|
+
/\.\w+/, // Property access
|
|
188
|
+
/==|!=|<|>|<=|>=/, // Comparison operators
|
|
189
|
+
/isAuthenticated\(\)/, // Authentication checks
|
|
190
|
+
/isAnonymous\(\)/, // Anonymous checks
|
|
191
|
+
/permitAll\(\)/, // Permit all
|
|
192
|
+
/denyAll\(\)/, // Deny all
|
|
193
|
+
/authentication\./i, // Authentication object access
|
|
194
|
+
/principal\./i, // Principal object access
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
const hasComplexPattern = complexPatterns.some((pattern) => pattern.test(expression));
|
|
198
|
+
|
|
199
|
+
return hasOtherContent || hasComplexPattern;
|
|
200
|
+
}
|