44 Commits
main ... natjav

Author SHA1 Message Date
Ingo Schnabel
ba4e5961b8 Item 260: a URL held in a fixed class property folds to its initializer 2026-09-26 22:57:27 +02:00
Ingo Schnabel
c05ac33e5a Item 259: an optional query string held in a const is the URL's query, not a path segment 2026-09-26 22:46:23 +02:00
Ingo Schnabel
3e923bed42 Roadmap: items 252-258 done, measured on server 399 2026-09-26 22:35:56 +02:00
Ingo Schnabel
129987a4c1 Docs: K3 gaps second round (items 252-258) in the agent guide 2026-09-26 21:55:42 +02:00
Ingo Schnabel
a0e23c6594 Item 258: JAXB types of a WSDL's schemas are linked to it; soap-endpoints lists typesPackages and their users as clients 2026-09-26 21:47:12 +02:00
Ingo Schnabel
0264bd554e Item 256: NatJav view columns get READS/WRITES (also through aliases); DDM columns count reads/writes across all views 2026-09-26 21:39:50 +02:00
Ingo Schnabel
0066f9f717 Item 257: db-tables/{ddm}/columns no longer lists a NatJav data area's view as a column 2026-09-26 21:10:16 +02:00
Ingo Schnabel
2207775c73 Item 255: dynamic-calls/overrides anchored on the call site's module and includers (55 M -> 6 k db hits on korin) 2026-09-26 21:05:27 +02:00
Ingo Schnabel
7ef7fe4a89 Item 253: spec-first Spring servers - swagger.yaml operations make their delegate methods REST endpoints 2026-09-26 20:53:58 +02:00
Ingo Schnabel
af189ee706 Item 254: mapping paths built from constants with + are resolved; an unresolvable path is no endpoint 2026-09-26 19:43:57 +02:00
Ingo Schnabel
eea2cbf900 Item 252: OpenAPI counterpart match compares the generator's camel-cased tag (i18n -> I18nApiDelegate) 2026-09-26 19:32:23 +02:00
Ingo Schnabel
9bd6c5f3ae Roadmap: items 252-258, the gaps still open in agenticcode-luecken.md against server 394 2026-09-26 19:20:01 +02:00
Ingo Schnabel
ccf2a9defb UI: regenerate the OpenAPI client schema (stale since 2026-08-09) 2026-09-26 18:41:41 +02:00
Ingo Schnabel
62abc1ed86 Docs: refresh, recreate and refresh-jobs in the API quick reference and agent system prompt (item 242) 2026-09-26 18:41:41 +02:00
Ingo Schnabel
f54a829054 Server version 394 (package D deployed) 2026-09-26 18:35:47 +02:00
Ingo Schnabel
98b6549308 Item 248: restart policy for neo4j, recovery after a simulated crash measured 2026-09-26 18:32:10 +02:00
Ingo Schnabel
2b38e466b4 Item 242: refreshes as jobs - async refresh/recreate, refresh-jobs status, 409 for a second refresh 2026-09-26 18:27:34 +02:00
Ingo Schnabel
de5ca4428c Item 250 closed: CONTAINS reap test, node properties documented as additive 2026-09-26 18:11:01 +02:00
Ingo Schnabel
0a9718399a Roadmap: live timing of the REST counterpart step, server version 390 2026-09-26 17:52:47 +02:00
Ingo Schnabel
cfb367a1bc Docs: NGXS limit for one action handled by two states of one file (item 232) 2026-09-26 17:50:38 +02:00
Ingo Schnabel
52f2f4ae36 REST counterparts: backend handlers collected once per counterpart; measurements for items 232-234, 241 2026-09-26 17:47:14 +02:00
Ingo Schnabel
15e2a16eaf Counterparts: suffix match needs the handler's application, OpenAPI match by generated method name (item 234) 2026-09-26 17:31:31 +02:00
Ingo Schnabel
7a33dfd956 Roadmap: the later items 182-191 become 242-251, references updated 2026-09-26 16:49:54 +02:00
Ingo Schnabel
ef24c8c20a Angular: nested SCSS rules with design tokens, inline template styles (item 233) 2026-09-26 13:49:09 +02:00
Ingo Schnabel
6650db68b9 Angular: NGXS states, actions, dispatch, selectors and ngxsForm on the store model (item 232) 2026-09-26 13:15:43 +02:00
Ingo Schnabel
281b7a4298 Angular: HttpClient and in-memory OpenAPI client endpoints, paired with the backend (item 234) 2026-09-26 12:09:40 +02:00
Ingo Schnabel
2b208fa9d0 excludeDirs: /-prefixed entries are anchored at the root (item 241) 2026-09-26 11:30:08 +02:00
Ingo Schnabel
f31892b345 Roadmap: measurements for items 238 and 239 on korin-ws and k3 2026-09-26 11:01:31 +02:00
Ingo Schnabel
55b978510d wsimport bindings link generated WS client packages to their WSDL (item 239) 2026-09-26 10:22:33 +02:00
Ingo Schnabel
abcdba91dd SOAP endpoints: EntireX port from services.xml and WSDL, ?port= filter (item 238) 2026-09-26 10:18:19 +02:00
Ingo Schnabel
54065cb203 callers/callees show counterparts across projects (item 237) 2026-09-26 10:12:40 +02:00
Ingo Schnabel
ad9dcab09d /modules: countOnly, limit and offset (item 240) 2026-09-26 10:09:09 +02:00
Ingo Schnabel
0ec965b2af NatJav: FDT fields, DDM to FDT link, table columns and file number, table unresolved flag (item 236) 2026-09-26 09:41:22 +02:00
Ingo Schnabel
c8d4ff6120 Roadmap: K3 analysis gaps L1-L6 as high-priority items 236-240 and 234 2026-09-26 08:01:27 +02:00
Ingo Schnabel
edd092a176 Angular: form fields and grid columns bound to DTO fields, DTO classes as data structures (item 231) 2026-09-26 07:59:27 +02:00
Ingo Schnabel
1925d964df Angular: routes and dialogs, GET /screens (item 230) 2026-09-25 19:29:58 +02:00
Ingo Schnabel
664eb35227 Angular support: project language angular, components, templates, DI (item 229) 2026-09-25 17:55:11 +02:00
Ingo Schnabel
5848f32bf7 Java parser: Spring MVC endpoints (item 235); roadmap items 229-234 planned 2026-09-25 17:14:39 +02:00
Ingo Schnabel
d1fbea247a Project delete: remove relationships in batches before nodes (item 228) 2026-09-25 15:54:26 +02:00
Ingo Schnabel
9c39f8f3eb KORIN gaps 225/226: SOAP endpoints and counterparts, configuration files with masked secrets 2026-09-25 15:21:00 +02:00
Ingo Schnabel
89dcf2bbaa NatJav/KORIN: query-time CHA, warm fixes, KORIN gap items 204-227
- 204/205: class hierarchy resolved at query time; fan-out warm skips empty paths
- 206-211: FETCH/INPUT_MAP/STACK call kinds, work files, headers, extends/unresolved/exact filters, mappedBy
- 214: korin-ws skeleton -> NatJav facade counterparts
- 215-223: copycode modules, statements + transactions, dispatch guards, fan-in, db-tables, traits, literal args
- 224/227: index-served identifier search, NatJav data areas not warmed, annotation attributes
2026-09-25 13:52:58 +02:00
Ingo Schnabel
443c213459 Workflow: commit after each verified task; git commands ask, push stays denied 2026-09-25 13:52:41 +02:00
Ingo Schnabel
c054ab5e48 Natjav 2026-09-24 18:01:13 +02:00
Ingo Schnabel
3f63c94469 Natjav 2026-09-24 16:36:15 +02:00
195 changed files with 24255 additions and 769 deletions

View File

@@ -1,13 +1,13 @@
{
"permissions": {
"deny": [
"Bash(git commit:*)",
"Bash(git push:*)"
],
"ask": [
"Bash(rm:*)",
"Bash(rmdir:*)",
"Bash(git:*)"
"Bash(git:*)",
"Bash(git commit:*)"
]
}
}

View File

@@ -6,3 +6,5 @@
!ac-parser-typescript/sidecar/package.json
!ac-parser-typescript/sidecar/package-lock.json
!ac-parser-typescript/sidecar/extract.mjs
!ac-parser-typescript/sidecar/angular.mjs
!ac-parser-typescript/sidecar/openapi.mjs

3
.gitignore vendored
View File

@@ -45,3 +45,6 @@ replay_pid*
/.mvn/
/.idea/
# local symlink to external project
/korin-natjav

View File

@@ -38,11 +38,10 @@
- Remove dead/unreachable code immediately (ask first if unsure).
- Be willing to backtrack if heading in the wrong direction.
- Trivial tasks: Ask "Trivial? Direct edit or full workflow?"
- Never run `git commit` (or `git push`) without the user explicitly asking for it in that turn. A prior commit approval
does not carry over to later changes.
- **The human decides what to commit and when.** Committing is a human decision, not yours. Do not stage, group, or
propose commits on your own initiative, and do not nudge toward committing. You may prepare a suggested commit message
when asked, but the act — and its timing and scope — is the human's call alone.
- **Commit after every completed task** (decided 2026-09-25): once a task is done and verified (tests green), create one
`git commit` with a short, meaningful message covering exactly that task. Every git command asks for permission first
(`.claude/settings.json`: `Bash(git:*)` is `ask`), so the human approves each commit as it happens.
- Never run `git push` — it stays denied in `.claude/settings.json`; pushing is the human's call alone.
- All features must be tracked in `x-docs/roadmap.md`. Once a feature has been implemented, mark it `[x]` and add a
timestamp (date) indicating when it was completed.
- For **every feature** that changes API behavior or what an agent can query, you MUST update
@@ -80,7 +79,7 @@
1. **PROPOSAL** – Clear plan + impact. Wait for `OK PROPOSAL`.
2. **VALIDATE** – Brutal self-critique (NullAway, layering, regressions). Wait for `OK VALIDATE`.
3. **IMPLEMENT** – One unit at a time. Show exact diff only.
4. **VERIFICATION** – Run relevant tests. Suggest a commit message, but do not commit unless asked (see Core Rules).``
4. **VERIFICATION** – Run relevant tests; when green, commit the task with a short message (see Core Rules).``
Quarkus-based server for parsing, storing, and agentically querying source code (Natural/Software AG and Java).
Programs are parsed into a unified AST, persisted as a graph in Neo4j, enriched with semantic information, and exposed via an agent-optimized API.

View File

@@ -22,6 +22,7 @@ import java.util.concurrent.Callable;
UseCommand.class,
ProjectCommand.class,
RefreshCommand.class,
RefreshJobsCommand.class,
CallersCommand.class,
FunctionCallersCommand.class,
CalleesCommand.class,
@@ -31,6 +32,12 @@ import java.util.concurrent.Callable;
EgoGraphCommand.class,
DbAccessesCommand.class,
WorkfileAccessesCommand.class,
TransactionsCommand.class,
SearchStatementCommand.class,
CallGraphAggregateCommand.class,
DbTablesCommand.class,
SoapEndpointsCommand.class,
ScreensCommand.class,
SqlStatementsCommand.class,
ContextCommand.class,
FunctionsCommand.class,

View File

@@ -0,0 +1,41 @@
package com.agenticcode.cli;
import org.jspecify.annotations.Nullable;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
/**
* Item 218: the module call graph rolled up by directory — how many module pairs call from one
* directory into another, at a chosen depth of the path.
*/
@Command(name = "call-graph-aggregate", mixinStandardHelpOptions = true,
description = "Module call graph rolled up by directory (CALLS_MODULE pairs per directory prefix)")
final class CallGraphAggregateCommand extends AbstractProjectCommand {
@Option(names = "--level", description = "Directory segments that form a group (default 1)")
int level = -1;
@Option(names = "--count-only", description = "Print only how many group pairs there are")
boolean countOnly;
@Option(names = "--limit", description = "Max items to return")
int limit = -1;
@Option(names = "--offset", description = "Items to skip")
int offset = -1;
@Override
public Integer call() throws Exception {
try {
String path = appendQuery(projectPath() + "/call-graph/aggregate?groupBy=path", "level", level);
if (countOnly) {
path = appendQuery(path, "countOnly", "true");
}
path = appendQuery(appendQuery(path, "limit", limit), "offset", offset);
return printResponse(apiClient().get(path));
} catch (IllegalStateException e) {
System.err.println(e.getMessage());
return 1;
}
}
}

View File

@@ -7,7 +7,8 @@ import picocli.CommandLine.Option;
/**
* Item 193: lists a project's counterparts in another project — its outbound web-service calls with
* the backend handler serving them, its generated DTOs with the Java class they mirror, and their
* fields — or, with {@code --unmatched}, exactly those that have no twin yet.
* fields, its calls into NatJav programs (item 214) — or, with {@code --unmatched}, exactly those
* that have no twin yet.
*/
@Command(name = "counterparts", mixinStandardHelpOptions = true,
description = "List the project's COUNTERPART_OF twins in its counterpart project(s): web-service calls -> handlers, generated DTOs -> classes, fields -> fields")
@@ -16,12 +17,15 @@ final class CounterpartsCommand extends AbstractProjectCommand {
@Option(names = "--module", description = "Only the rows declared in this module (identity or short name)")
@Nullable String module;
@Option(names = "--kind", description = "rest | dto | field (default: all)")
@Option(names = "--kind", description = "rest | dto | field | natjav | soap (default: all)")
@Nullable String kind;
@Option(names = "--unmatched", description = "Only rows without a counterpart (what is not served / mirrored yet)")
boolean unmatched;
@Option(names = "--counterpart-module", description = "Only rows whose twin is in this module of the counterpart project (e.g. which skeleton calls NatJav facade X)")
@Nullable String counterpartModule;
@Option(names = "--count-only", description = "Print only how many rows match, instead of the rows themselves")
boolean countOnly;
@@ -36,6 +40,7 @@ final class CounterpartsCommand extends AbstractProjectCommand {
try {
String path = appendQuery(projectPath() + "/counterparts", "module", module);
path = appendQuery(path, "kind", kind);
path = appendQuery(path, "counterpartModule", counterpartModule);
if (unmatched) {
path = appendQuery(path, "unmatched", "true");
}

View File

@@ -5,9 +5,10 @@ import picocli.CommandLine.Parameters;
/**
* Lists the columns of a DB table, derived from the data structure used in
* {@code SELECT ... INTO VIEW} statements against that table.
* {@code SELECT ... INTO VIEW} statements against that table, a JPA entity, or (item 236) the NatJav
* DDM mapping it, with ADABAS short name, FDT format/length and options.
*/
@Command(name = "db-table-columns", mixinStandardHelpOptions = true, description = "List the columns of a DB table")
@Command(name = "db-table-columns", mixinStandardHelpOptions = true, description = "List the columns of a DB table (NatJav: with ADABAS short name, FDT length and options)")
final class DbTableColumnsCommand extends AbstractProjectCommand {
@SuppressWarnings("NullAway.Init")

View File

@@ -0,0 +1,47 @@
package com.agenticcode.cli;
import org.jspecify.annotations.Nullable;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
/**
* Item 219: lists a project's tables with the modules that map them and, with {@code --stats}, their
* readers, writers and statement counts. Item 236: a NatJav table also shows its FDT and ADABAS file number.
*/
@Command(name = "db-tables", mixinStandardHelpOptions = true,
description = "List the project's DB tables with their mapping modules, FDT and file number (with --stats: readers, writers, statement counts)")
final class DbTablesCommand extends AbstractProjectCommand {
@Option(names = "--name", description = "Case-insensitive fragment of the table name")
@Nullable String name;
@Option(names = "--stats", description = "Add reading/writing modules and statement counts")
boolean stats;
@Option(names = "--count-only", description = "Print only how many tables match")
boolean countOnly;
@Option(names = "--limit", description = "Max items to return")
int limit = -1;
@Option(names = "--offset", description = "Items to skip")
int offset = -1;
@Override
public Integer call() throws Exception {
try {
String path = appendQuery(projectPath() + "/db-tables", "name", name);
if (stats) {
path = appendQuery(path, "stats", "true");
}
if (countOnly) {
path = appendQuery(path, "countOnly", "true");
}
path = appendQuery(appendQuery(path, "limit", limit), "offset", offset);
return printResponse(apiClient().get(path));
} catch (IllegalStateException e) {
System.err.println(e.getMessage());
return 1;
}
}
}

View File

@@ -1,5 +1,6 @@
package com.agenticcode.cli;
import org.jspecify.annotations.Nullable;
import picocli.CommandLine.Command;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
@@ -40,10 +41,14 @@ final class DynamicCallsCommand implements Callable<Integer> {
@Command(name = "unresolved", mixinStandardHelpOptions = true,
description = "List unresolved dynamic CALLNAT call sites (originFile+lineNo to pin)")
static final class UnresolvedCommand extends AbstractProjectCommand {
@Option(names = "--group-by", description = "variable: one row per dispatch variable (item 220)")
@Nullable String groupBy;
@Override
public Integer call() throws Exception {
try {
return printResponse(apiClient().get(projectPath() + "/dynamic-calls/unresolved"));
return printResponse(apiClient().get(appendQuery(projectPath() + "/dynamic-calls/unresolved", "groupBy", groupBy)));
} catch (IllegalStateException e) {
System.err.println(e.getMessage());
return 1;

View File

@@ -13,7 +13,7 @@ import picocli.CommandLine.Option;
description = "Per-language LoC/SLoC rollup of a project (plus total)")
final class LocCommand extends AbstractProjectCommand {
@Option(names = "--language", description = "Filter by language (natural/java)")
@Option(names = "--language", description = "Filter by language (natural/java/natjav)")
@Nullable String language;
@Option(names = "--source-file", description = "Filter by source file")

View File

@@ -5,8 +5,9 @@ import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
/**
* Lists the modules of a project, optionally filtered by source file, module kind, or a direct
* {@code extends} base class name.
* Lists the modules of a project, optionally filtered by source file, module kind, or an
* {@code extends} base class name (direct, or with {@code --transitive} every level below it). With
* {@code --unresolved} it lists the unresolved placeholders instead (item 209).
*/
@Command(name = "modules", mixinStandardHelpOptions = true, description = "List the modules of a project")
final class ModulesCommand extends AbstractProjectCommand {
@@ -20,6 +21,30 @@ final class ModulesCommand extends AbstractProjectCommand {
@Option(names = "--extends", description = "Filter by direct EXTENDS base class name")
@Nullable String extendsName;
@Option(names = "--transitive", description = "With --extends: every subclass below the base, not only the direct ones")
boolean transitive;
@Option(names = "--unresolved", description = "List the unresolved placeholder modules instead of the real ones")
boolean unresolved;
@Option(names = "--with-caller-count", description = "Report callerCount (distinct calling modules) on every row")
boolean withCallerCount;
@Option(names = "--no-callers", description = "Only modules nothing calls (entry points or dead code)")
boolean noCallers;
@Option(names = "--sort", description = "fanIn (most-called first) or name (default)")
@Nullable String sort;
@Option(names = "--count-only", description = "Print only how many modules match (item 240)")
boolean countOnly;
@Option(names = "--limit", description = "Max modules to return (default: all)")
int limit = -1;
@Option(names = "--offset", description = "Modules to skip")
int offset = -1;
@Override
public Integer call() throws Exception {
try {
@@ -35,7 +60,31 @@ final class ModulesCommand extends AbstractProjectCommand {
}
if (extendsName != null) {
path += separator + "extends=" + encode(extendsName);
separator = "&";
}
if (transitive) {
path += separator + "transitive=true";
separator = "&";
}
if (unresolved) {
path += separator + "unresolved=true";
separator = "&";
}
if (withCallerCount) {
path += separator + "withCallerCount=true";
separator = "&";
}
if (noCallers) {
path += separator + "noCallers=true";
separator = "&";
}
if (sort != null) {
path += separator + "sort=" + encode(sort);
}
if (countOnly) {
path = appendQuery(path, "countOnly", "true");
}
path = appendQuery(appendQuery(path, "limit", limit), "offset", offset);
return printResponse(apiClient().get(path));
} catch (IllegalStateException e) {
System.err.println(e.getMessage());

View File

@@ -55,12 +55,13 @@ final class ProjectCommand implements Callable<Integer> {
String description = "";
@Option(names = {"-x", "--exclude-dir"},
description = "Directory name to skip when scanning the root (case-insensitive); repeatable")
description = "Directory to skip when scanning the root (case-insensitive): a bare name anywhere, "
+ "a /-prefixed entry (/Koala) only at the root; repeatable")
List<String> excludeDirs = List.of();
@SuppressWarnings("NullAway.Init")
@Option(names = {"-l", "--language"}, required = true,
description = "Project source language (natural/java/typescript)")
description = "Project source language (natural/java/natjav/typescript/angular)")
String language;
@Option(names = {"-g", "--generated-dir"},
@@ -100,10 +101,11 @@ final class ProjectCommand implements Callable<Integer> {
String root = "";
@Option(names = {"-x", "--exclude-dir"},
description = "Directory name to skip when scanning the root (case-insensitive); repeatable")
description = "Directory to skip when scanning the root (case-insensitive): a bare name anywhere, "
+ "a /-prefixed entry (/Koala) only at the root; repeatable")
List<String> excludeDirs = List.of();
@Option(names = {"-l", "--language"}, description = "Project source language (natural/java/typescript)")
@Option(names = {"-l", "--language"}, description = "Project source language (natural/java/natjav/typescript/angular)")
String language = "";
@Option(names = {"-g", "--generated-dir"},
@@ -220,10 +222,27 @@ final class ProjectCommand implements Callable<Integer> {
@Option(names = "--deep", description = "Run the full field-level ingest (slower)")
boolean deep;
@Option(names = "--async", description = "Return the job at once (HTTP 202); follow it with 'refresh-jobs <id>'")
boolean async;
@Option(names = "--wait", description = "Run as a job and poll until it ends, printing each phase to stderr")
boolean await;
@Override
public Integer call() throws Exception {
String path = "/api/projects/" + encode(name) + "/recreate" + (deep ? "?deep=true" : "");
return printResponse(apiClient().post(path));
String projectPath = "/api/projects/" + encode(name);
String path = projectPath + "/recreate" + (deep ? "?deep=true" : "");
if (async || await) {
path = appendQuery(path, "async", "true");
}
ApiClient client = apiClient();
ApiClient.ApiResponse response = client.post(path);
if (await) {
response = RefreshJobWait.follow(client, projectPath, response);
int exit = printResponse(response);
return RefreshJobWait.failed(response) ? 1 : exit;
}
return printResponse(response);
}
}

View File

@@ -56,6 +56,16 @@ final class RefreshCommand extends AbstractProjectCommand {
+ "timings are not comparable to a normal refresh")
boolean profile;
@Option(names = "--async",
description = "Whole-project refresh only: return the job at once (HTTP 202) instead of waiting; "
+ "follow it with 'refresh-jobs <id>'. For runs that may outlive the 30-minute HTTP idle timeout")
boolean async;
@Option(names = "--wait",
description = "Whole-project refresh only: start it as a job and poll until it ends, printing each phase "
+ "to stderr - immune to the HTTP idle timeout")
boolean await;
@Override
public Integer call() throws Exception {
if (name != null && !name.isBlank()) {
@@ -85,6 +95,16 @@ final class RefreshCommand extends AbstractProjectCommand {
if (profile) {
path = appendQuery(path, "profile", "true");
}
return printResponse(apiClient().post(path));
if (async || await) {
path = appendQuery(path, "async", "true");
}
ApiClient client = apiClient();
ApiClient.ApiResponse response = client.post(path);
if (await) {
response = RefreshJobWait.follow(client, projectPath(), response);
int exit = printResponse(response);
return RefreshJobWait.failed(response) ? 1 : exit;
}
return printResponse(response);
}
}

View File

@@ -0,0 +1,61 @@
package com.agenticcode.cli;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Objects;
/**
* Item 242: follows an asynchronous refresh job until it ends. Prints each new phase to stderr (so the
* final JSON on stdout stays pipeable) and returns the finished job's response.
*/
final class RefreshJobWait {
private static final ObjectMapper MAPPER = new ObjectMapper();
static final long POLL_MILLIS = 5_000;
private RefreshJobWait() {
}
/**
* @param started the {@code 202} answer of {@code POST /refresh?async=true} (or {@code /recreate});
* any other answer is returned unchanged
*/
static ApiClient.ApiResponse follow(ApiClient client, String projectPath, ApiClient.ApiResponse started)
throws IOException, InterruptedException {
if (started.statusCode() != 202) {
return started;
}
String id = MAPPER.readTree(started.body()).path("id").asText();
String path = projectPath + "/refresh-jobs/" + AbstractApiCommand.encode(id);
String lastPhase = "";
while (true) {
ApiClient.ApiResponse response = client.get(path);
if (!response.isSuccess()) {
return response;
}
JsonNode job = MAPPER.readTree(response.body());
if (!"RUNNING".equals(job.path("status").asText())) {
return response;
}
String phase = job.path("progress").asText("");
if (!phase.isEmpty() && !Objects.equals(phase, lastPhase)) {
System.err.println("[" + job.path("elapsedSeconds").asLong() + " s] " + phase);
lastPhase = phase;
}
Thread.sleep(POLL_MILLIS);
}
}
/**
* A finished job whose status is {@code FAILED} is an error exit even though the poll answered 200.
*/
static boolean failed(ApiClient.ApiResponse response) {
try {
return response.isSuccess() && "FAILED".equals(MAPPER.readTree(response.body()).path("status").asText());
} catch (IOException e) {
return false;
}
}
}

View File

@@ -0,0 +1,24 @@
package com.agenticcode.cli;
import org.jspecify.annotations.Nullable;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
/**
* Item 242: the project's refresh jobs ({@code GET /refresh-jobs}), or one of them by id
* ({@code GET /refresh-jobs/{id}}) — status, the current phase while it runs, the ingest summary once
* it ends.
*/
@Command(name = "refresh-jobs", mixinStandardHelpOptions = true,
description = "List the project's recent refresh jobs, or show one by id (status, current phase, summary when done)")
final class RefreshJobsCommand extends AbstractProjectCommand {
@Parameters(index = "0", arity = "0..1", description = "Job id (from 'refresh --async'); omit to list the recent jobs")
@Nullable String id;
@Override
public Integer call() throws Exception {
String path = projectPath() + "/refresh-jobs" + (id == null || id.isBlank() ? "" : "/" + encode(id));
return printResponse(apiClient().get(path));
}
}

View File

@@ -0,0 +1,45 @@
package com.agenticcode.cli;
import org.jspecify.annotations.Nullable;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
/**
* Item 230: lists the screens of an Angular front end — routes with their component, title and guards,
* and dialogs with every place that opens them.
*/
@Command(name = "screens", mixinStandardHelpOptions = true,
description = "List the routes and dialogs of an Angular project")
final class ScreensCommand extends AbstractProjectCommand {
@Option(names = "--kind", description = "route | dialog (default: both)")
@Nullable String kind;
@Option(names = "--component", description = "Only screens showing this component (module, file name or class)")
@Nullable String component;
@Option(names = "--count-only", description = "Print only how many screens match")
boolean countOnly;
@Option(names = "--limit", description = "Max items to return")
int limit = -1;
@Option(names = "--offset", description = "Items to skip")
int offset = -1;
@Override
public Integer call() throws Exception {
try {
String path = appendQuery(projectPath() + "/screens", "kind", kind);
path = appendQuery(path, "component", component);
if (countOnly) {
path = appendQuery(path, "countOnly", "true");
}
path = appendQuery(appendQuery(path, "limit", limit), "offset", offset);
return printResponse(apiClient().get(path));
} catch (IllegalStateException e) {
System.err.println(e.getMessage());
return 1;
}
}
}

View File

@@ -6,8 +6,8 @@ import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
/**
* Finds nodes (Java only) carrying an annotation whose name matches (substring, case-insensitive),
* optionally filtered by node type.
* Finds nodes (Java only) carrying an annotation whose name matches (substring, case-insensitive, or
* with {@code --exact} the whole name, item 210), optionally filtered by node type.
*/
@Command(name = "search-annotation", mixinStandardHelpOptions = true, description = "Search for nodes carrying a matching annotation")
final class SearchAnnotationCommand extends AbstractProjectCommand {
@@ -19,6 +19,15 @@ final class SearchAnnotationCommand extends AbstractProjectCommand {
@Option(names = "--type", description = "Filter by node type (MODULE, FUNCTION, VARIABLE, DATA_STRUCTURE, DB_TABLE)")
@Nullable String type;
@Option(names = "--attribute", description = "Only where the matched annotation has this attribute (item 227)")
@Nullable String attribute;
@Option(names = "--value", description = "Only where the matched annotation has an attribute of this value (item 227)")
@Nullable String value;
@Option(names = "--exact", description = "Match the whole annotation name (case-sensitive) instead of a substring")
boolean exact;
@Option(names = "--count-only", description = "Print only how many rows match, instead of the rows themselves")
boolean countOnly;
@@ -35,6 +44,11 @@ final class SearchAnnotationCommand extends AbstractProjectCommand {
if (type != null) {
path += "&type=" + encode(type);
}
if (exact) {
path = appendQuery(path, "exact", "true");
}
path = appendQuery(path, "attribute", attribute);
path = appendQuery(path, "value", value);
if (countOnly) {
path = appendQuery(path, "countOnly", "true");
}

View File

@@ -0,0 +1,45 @@
package com.agenticcode.cli;
import org.jspecify.annotations.Nullable;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
/**
* Item 221: finds statement sites by kind — {@code END_TRANSACTION}, {@code BACKOUT_TRANSACTION},
* {@code REINPUT}, {@code SET_KEY}, {@code STACK_DATA}, {@code ON_ERROR} — optionally in one module.
*/
@Command(name = "search-statement", mixinStandardHelpOptions = true,
description = "Find statements by kind (END_TRANSACTION, BACKOUT_TRANSACTION, REINPUT, SET_KEY, STACK_DATA, ON_ERROR)")
final class SearchStatementCommand extends AbstractProjectCommand {
@Option(names = "--kind", description = "Statement kind(s), comma-separated (default: all)")
@Nullable String kind;
@Option(names = "--module", description = "Only this module")
@Nullable String module;
@Option(names = "--count-only", description = "Print only how many sites match")
boolean countOnly;
@Option(names = "--limit", description = "Max items to return")
int limit = -1;
@Option(names = "--offset", description = "Items to skip")
int offset = -1;
@Override
public Integer call() throws Exception {
try {
String path = appendQuery(projectPath() + "/search/statement", "kind", kind);
path = appendQuery(path, "module", module);
if (countOnly) {
path = appendQuery(path, "countOnly", "true");
}
path = appendQuery(appendQuery(path, "limit", limit), "offset", offset);
return printResponse(apiClient().get(path));
} catch (IllegalStateException e) {
System.err.println(e.getMessage());
return 1;
}
}
}

View File

@@ -0,0 +1,49 @@
package com.agenticcode.cli;
import org.jspecify.annotations.Nullable;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
/**
* Item 225: lists a project's SOAP endpoints — Axis2 services, WSDL contracts, JAX-WS classes — one row
* per operation, with the counterpart service in another project.
*/
@Command(name = "soap-endpoints", mixinStandardHelpOptions = true,
description = "List SOAP endpoints (Axis2 services.xml, WSDL, @WebService) with their counterpart service")
final class SoapEndpointsCommand extends AbstractProjectCommand {
@Option(names = "--service", description = "Only this service")
@Nullable String service;
@Option(names = "--port", description = "Only services exposing this EntireX port, e.g. WSK369 (item 238)")
@org.jspecify.annotations.Nullable String port;
@Option(names = "--kind", description = "axis2 | wsdl | jaxws (default: all)")
@Nullable String kind;
@Option(names = "--count-only", description = "Print only how many rows match")
boolean countOnly;
@Option(names = "--limit", description = "Max items to return")
int limit = -1;
@Option(names = "--offset", description = "Items to skip")
int offset = -1;
@Override
public Integer call() throws Exception {
try {
String path = appendQuery(projectPath() + "/soap-endpoints", "service", service);
path = appendQuery(path, "kind", kind);
path = appendQuery(path, "port", port);
if (countOnly) {
path = appendQuery(path, "countOnly", "true");
}
path = appendQuery(appendQuery(path, "limit", limit), "offset", offset);
return printResponse(apiClient().get(path));
} catch (IllegalStateException e) {
System.err.println(e.getMessage());
return 1;
}
}
}

View File

@@ -8,13 +8,13 @@ import picocli.CommandLine.Option;
* Item 196: the style inventory — sx/style/styled blocks and CSS rules with keys, literals and theme tokens.
*/
@Command(name = "styles", mixinStandardHelpOptions = true,
description = "List style blocks (sx, style, styled, css rules) with their CSS keys, hard-coded literals and theme tokens")
description = "List style blocks (sx, style, styled, css/scss rules, Angular template styles) with their CSS keys, hard-coded literals and theme tokens")
final class StylesCommand extends AbstractProjectCommand {
@Option(names = "--module", description = "Only blocks in this module (identity or short name)")
@Nullable String module;
@Option(names = "--kind", description = "sx | style | styled | css (default: all)")
@Option(names = "--kind", description = "sx | style | styled | css | scss | template (default: all)")
@Nullable String kind;
@Option(names = "--with-literals", description = "Only blocks with hard-coded colour/length literals (what bypasses the theme)")

View File

@@ -0,0 +1,37 @@
package com.agenticcode.cli;
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
/**
* Item 216: lists the transaction boundaries ({@code END TRANSACTION} / {@code BACKOUT TRANSACTION})
* of a module and, with {@code --depth}, of the modules it reaches within that many module hops.
*/
@Command(name = "transactions", mixinStandardHelpOptions = true,
description = "List END/BACKOUT TRANSACTION sites of a module (and of the modules it reaches, with --depth)")
final class TransactionsCommand extends AbstractProjectCommand {
@SuppressWarnings("NullAway.Init")
@Parameters(index = "0", description = "Module name")
String moduleName;
@Mixin
ModuleSelector selector = new ModuleSelector();
@Option(names = "--depth", description = "Module hops to follow (default 0 = the module only)")
int depth = -1;
@Override
public Integer call() throws Exception {
try {
String path = projectPath() + "/modules/" + encode(moduleName) + "/transactions";
path = appendQuery(path, "depth", depth);
return printResponse(apiClient().get(selector.append(path)));
} catch (IllegalStateException e) {
System.err.println(e.getMessage());
return 1;
}
}
}

View File

@@ -4,4 +4,4 @@
server.url=http://localhost:8787
# Stamped by manage-ac.sh (stamp_cli_version) from ac-code-server's agenticcode.version
# at build time. "dev" means this jar wasn't built via manage-ac.sh.
version=334
version=401

View File

@@ -29,6 +29,10 @@
<groupId>com.agenticcode</groupId>
<artifactId>ac-parser-java</artifactId>
</dependency>
<dependency>
<groupId>com.agenticcode</groupId>
<artifactId>ac-parser-natjav</artifactId>
</dependency>
<dependency>
<groupId>com.agenticcode</groupId>
<artifactId>ac-parser-typescript</artifactId>
@@ -73,6 +77,11 @@
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>

View File

@@ -9,7 +9,7 @@ FROM node:24-slim AS sidecar
WORKDIR /sidecar
COPY ac-parser-typescript/sidecar/package.json ac-parser-typescript/sidecar/package-lock.json ./
RUN npm ci --omit=dev --no-audit --no-fund
COPY ac-parser-typescript/sidecar/extract.mjs ./
COPY ac-parser-typescript/sidecar/extract.mjs ac-parser-typescript/sidecar/angular.mjs ac-parser-typescript/sidecar/openapi.mjs ./
# --- stage 2: the server ------------------------------------------------------------------------
FROM eclipse-temurin:21-jre

View File

@@ -58,10 +58,12 @@ public class AnalysisResource {
*/
private final int callTreeInternalBudget;
private final SourceSearchService sourceSearchService;
private final RefreshJobs refreshJobs;
public AnalysisResource(ProjectIngestService projectIngestService, ProjectRootResolver rootResolver,
GraphRepository graphRepository, DeepIngestCoordinator deepIngestCoordinator,
SourceSnippetService sourceSnippetService, SourceSearchService sourceSearchService,
RefreshJobs refreshJobs,
@ConfigProperty(name = "agenticcode.call-tree.default-depth", defaultValue = "3") int defaultCallTreeDepth,
@ConfigProperty(name = "agenticcode.call-tree.max-depth", defaultValue = "10") int maxCallTreeDepth,
@ConfigProperty(name = "agenticcode.call-tree.internal-budget", defaultValue = "20") int callTreeInternalBudget) {
@@ -71,6 +73,7 @@ public class AnalysisResource {
this.deepIngestCoordinator = deepIngestCoordinator;
this.sourceSnippetService = sourceSnippetService;
this.sourceSearchService = sourceSearchService;
this.refreshJobs = refreshJobs;
this.defaultCallTreeDepth = defaultCallTreeDepth;
this.maxCallTreeDepth = maxCallTreeDepth;
this.callTreeInternalBudget = callTreeInternalBudget;
@@ -105,7 +108,7 @@ public class AnalysisResource {
return ProjectResource.error(status, failed.code(), failed.message());
}
}
List<ModuleInfo> modules = graphRepository.listModules(project, null, null, null).await().indefinitely();
List<ModuleInfo> modules = graphRepository.listModules(project, null, null).await().indefinitely();
return Response.ok(sourceSearchService.search(root, modules, pattern, effectiveLimit)).build();
}
@@ -138,8 +141,12 @@ public class AnalysisResource {
* failures to structured errors: {@code 404 PROJECT_NOT_FOUND}, {@code 400 ROOT_NOT_SET} (no root
* configured), {@code 400 ROOT_NOT_FOUND} (root isn't a directory on the server), or
* {@code 500 INGEST_FAILED} (the root could not be walked).
*
* <p>Item 242: every refresh runs as a {@link RefreshJobs} job — one per project at a time, so a
* second one gets {@code 409 REFRESH_RUNNING} naming the running job instead of racing it.
* {@code async} answers {@code 202} with the job at once; poll {@code GET /refresh-jobs/{id}}.
*/
private Response withResolvedRoot(String project, RootIngest ingest) {
private Response withResolvedRoot(String project, String kind, boolean async, RootIngest ingest) {
ProjectInfo info;
switch (rootResolver.resolve(project)) {
case ProjectRootResolver.Resolved r -> info = r.project();
@@ -150,7 +157,21 @@ public class AnalysisResource {
}
}
try {
return Response.ok(ingest.run(info)).build();
if (async) {
return Response.accepted(refreshJobs.start(project, kind, () -> ingest.run(info))).build();
}
return Response.ok(refreshJobs.runNow(project, kind, () -> ingest.run(info))).build();
} catch (RefreshJobs.RefreshRunningException e) {
RefreshJobs.RefreshJob running = e.running();
Map<String, Object> details = new LinkedHashMap<>();
details.put("jobId", running.id());
details.put("kind", running.kind());
details.put("startedAt", running.startedAt());
if (running.progress() != null) {
details.put("progress", running.progress());
}
return ProjectResource.error(Response.Status.CONFLICT, "REFRESH_RUNNING", "A " + running.kind() + " of project '" + project
+ "' is already running (job " + running.id() + ")", details);
} catch (IOException e) {
return ProjectResource.error(Response.Status.INTERNAL_SERVER_ERROR, "INGEST_FAILED",
"Failed to scan root '" + info.root() + "': " + e.getMessage());
@@ -334,7 +355,16 @@ public class AnalysisResource {
@Operation(summary = "List unresolved dynamic CALLNAT call sites (item 82) a human/agent can pin a target to.")
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = UnresolvedDynamicCall.class)))
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public Uni<Response> unresolvedDynamicCalls(@PathParam("project") String project) {
public Uni<Response> unresolvedDynamicCalls(@PathParam("project") String project,
@Parameter(description = "Item 220: `variable` = one row per dispatch variable {variable, sites, modules, statementKinds}.")
@QueryParam("groupBy") @Nullable String groupBy) {
if (groupBy != null && !groupBy.isBlank()) {
if (!groupBy.strip().equalsIgnoreCase("variable")) {
return Uni.createFrom().item(ProjectResource.error(Response.Status.BAD_REQUEST, "GROUP_BY_UNSUPPORTED",
"Unknown groupBy '" + groupBy + "'; expected variable"));
}
return withProject(project, () -> graphRepository.groupUnresolvedDynamicCallsByVariable(project).map(this::ok));
}
return withProject(project, () -> graphRepository.listUnresolvedDynamicCalls(project).map(this::ok));
}
@@ -412,7 +442,11 @@ public class AnalysisResource {
@POST
@Path("/refresh")
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = IngestSummary.class)))
@APIResponse(responseCode = "202", description = "async=true: the refresh runs on as a job; poll GET /refresh-jobs/{id}.",
content = @Content(schema = @Schema(implementation = RefreshJobs.RefreshJob.class)))
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
@APIResponse(responseCode = "409", description = "REFRESH_RUNNING: a refresh of this project is already running; details carry its jobId.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public Response refresh(@PathParam("project") String project,
@QueryParam("deep") @Nullable Boolean deep,
@Parameter(description = "Item 129: comma-separated relative source paths to re-ingest instead of the whole root — "
@@ -429,21 +463,54 @@ public class AnalysisResource {
+ "and log the dominant operators of every step slower than 5 s, to attribute a step's time between matching "
+ "and writing. Profiling costs 10-30% on its own, so the run's absolute times are not comparable to a normal "
+ "refresh - the numbers describe the distribution inside a step. Off by default.")
@QueryParam("profile") @Nullable Boolean profile) {
@QueryParam("profile") @Nullable Boolean profile,
@Parameter(description = "Item 242: answer 202 with a job at once instead of holding the request until the "
+ "refresh ends; poll GET /refresh-jobs/{id} for its phase and, when it ends, its IngestSummary. For "
+ "refreshes that may outlive the 30-minute HTTP idle timeout.")
@QueryParam("async") @Nullable Boolean async) {
boolean deepIngest = deep != null && deep;
boolean detached = Boolean.TRUE.equals(async);
if (paths != null && !paths.isBlank()) {
List<String> requested = Arrays.stream(paths.split(",")).map(String::trim).filter(p -> !p.isEmpty()).toList();
if (requested.isEmpty()) {
return ProjectResource.error(Response.Status.BAD_REQUEST, "MISSING_PATHS",
"Query parameter 'paths' was given but contained no path");
}
return withResolvedRoot(project, info -> projectIngestService.refreshPaths(info, requested));
return withResolvedRoot(project, "refresh", detached, info -> projectIngestService.refreshPaths(info, requested));
}
return withResolvedRoot(project, info ->
return withResolvedRoot(project, deepIngest ? "deep refresh" : "refresh", detached, info ->
projectIngestService.refreshProject(info, deepIngest, Boolean.TRUE.equals(changedOnly),
Boolean.TRUE.equals(profile)));
}
/**
* Item 242: the project's recent refresh jobs (the last {@value RefreshJobs#KEEP} across the server,
* newest first) — running and finished, synchronous ones included.
*/
@GET
@Path("/refresh-jobs")
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = RefreshJobs.RefreshJob.class)))
public Response refreshJobs(@PathParam("project") String project) {
return Response.ok(refreshJobs.list(project)).build();
}
/**
* Item 242: one refresh job — status, current phase while it runs, {@link IngestSummary} when done.
*/
@GET
@Path("/refresh-jobs/{id}")
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = RefreshJobs.RefreshJob.class)))
@APIResponse(responseCode = "404", description = "JOB_NOT_FOUND: unknown id, evicted, or lost with a server restart.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public Response refreshJob(@PathParam("project") String project, @PathParam("id") String id) {
RefreshJobs.RefreshJob job = refreshJobs.get(project, id);
if (job == null) {
return ProjectResource.error(Response.Status.NOT_FOUND, "JOB_NOT_FOUND",
"No refresh job '" + id + "' for project '" + project + "' (unknown, evicted, or lost with a server restart)");
}
return Response.ok(job).build();
}
/**
* Item 78: re-initialise a project — throw its graph away and ingest it again from its own stored
* config, without ever asking for that config back.
@@ -471,10 +538,14 @@ public class AnalysisResource {
@APIResponse(responseCode = "400", description = "The project's stored root no longer resolves; nothing was deleted.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
@APIResponse(responseCode = "409", description = "REFRESH_RUNNING: a refresh of this project is already running; nothing was deleted.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public Response recreate(@PathParam("project") String project,
@QueryParam("deep") @Nullable Boolean deep) {
@QueryParam("deep") @Nullable Boolean deep,
@Parameter(description = "Item 242: answer 202 with a job at once; see POST /refresh.")
@QueryParam("async") @Nullable Boolean async) {
boolean deepIngest = deep != null && deep;
return withResolvedRoot(project, info -> {
return withResolvedRoot(project, "recreate", Boolean.TRUE.equals(async), info -> {
graphRepository.deleteProjectNodes(project).await().indefinitely();
return projectIngestService.refreshProject(info, deepIngest);
});
@@ -609,8 +680,42 @@ public class AnalysisResource {
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public Uni<Response> modules(@PathParam("project") String project, @QueryParam("sourceFile") @Nullable String sourceFile,
@QueryParam("moduleKind") @Nullable String moduleKind,
@QueryParam("extends") @Nullable String extendsName) {
return withProject(project, () -> graphRepository.listModules(project, sourceFile, moduleKind, extendsName).map(this::ok));
@QueryParam("extends") @Nullable String extendsName,
@Parameter(description = "Item 209: with extends, every subclass below the base, not only the direct ones.")
@QueryParam("transitive") @Nullable Boolean transitive,
@Parameter(description = "Item 209: list the unresolved placeholder modules (sourceFile \"\") instead of the real ones.")
@QueryParam("unresolved") @Nullable Boolean unresolved,
@Parameter(description = "Item 218: report callerCount (distinct calling modules) on every row.")
@QueryParam("withCallerCount") @Nullable Boolean withCallerCount,
@Parameter(description = "Item 218: only modules nothing calls (entry points or dead code).")
@QueryParam("noCallers") @Nullable Boolean noCallers,
@Parameter(description = "Item 218: `fanIn` = most-called first; `name` (default).")
@QueryParam("sort") @Nullable String sort,
@Parameter(description = "Item 240: page size. Absent = every module (the list stays unpaged by default).")
@QueryParam("limit") @Nullable Integer limit,
@QueryParam("offset") @Nullable Integer offset,
@Parameter(description = "Item 240: return only {count}.")
@QueryParam("countOnly") @Nullable Boolean countOnly) {
if (sort != null && !sort.isBlank() && !List.of("fanin", "name").contains(sort.strip().toLowerCase(java.util.Locale.ROOT))) {
return Uni.createFrom().item(ProjectResource.error(Response.Status.BAD_REQUEST, "SORT_UNSUPPORTED",
"Unknown sort '" + sort + "'; expected fanIn or name"));
}
ModuleListFanIn fanIn = new ModuleListFanIn(Boolean.TRUE.equals(withCallerCount), Boolean.TRUE.equals(noCallers),
sort != null && sort.strip().equalsIgnoreCase("fanIn"));
return withProject(project, () -> graphRepository.listModules(project, sourceFile, moduleKind, extendsName,
Boolean.TRUE.equals(transitive), Boolean.TRUE.equals(unresolved), fanIn).map(rows -> {
// Item 240: countOnly answered {count} only here; limit/offset are opt-in, so the web UI
// and every existing caller keep the full list.
if (isCountOnly(countOnly)) {
return countOnly(new Page<>(List.of(), rows.size(), false));
}
if (limit == null && offset == null) {
return ok(rows);
}
int from = Math.min(Math.max(offset == null ? 0 : offset, 0), rows.size());
int to = limit == null ? rows.size() : Math.min(rows.size(), from + Math.max(limit, 0));
return paged(new Page<>(rows.subList(from, to), rows.size(), to < rows.size()));
}));
}
/**
@@ -660,7 +765,7 @@ public class AnalysisResource {
@Parameter(description = "'module' (default) deep-ingests the module + its callees/data areas; 'neighborhood' additionally deep-ingests its transitive callers.")
@QueryParam("scope") @Nullable String scope) {
if (!"neighborhood".equalsIgnoreCase(scope)) {
return withResolvedRoot(project, info -> projectIngestService.refreshModule(info, name, maxDepth, maxNodes));
return withResolvedRoot(project, "module refresh", false, info -> projectIngestService.refreshModule(info, name, maxDepth, maxNodes));
}
// Neighbourhood scope: seed the ingest with the module's transitive callers AND callees
// (module-granularity CALLS closure, both directions) so the whole call-graph neighbourhood —
@@ -677,7 +782,7 @@ public class AnalysisResource {
}
}
int nodeBudget = maxNodes != null ? maxNodes : NEIGHBOURHOOD_NODE_BUDGET;
return withResolvedRoot(project, info -> projectIngestService.ingestModules(info, seeds, maxDepth, nodeBudget));
return withResolvedRoot(project, "module refresh", false, info -> projectIngestService.ingestModules(info, seeds, maxDepth, nodeBudget));
}
@GET
@@ -843,7 +948,8 @@ public class AnalysisResource {
@GET
@Path("/counterparts")
@Operation(summary = "Counterparts in another project (item 193)",
description = "This project's outbound web-service calls, generated data structures and their fields, each "
description = "This project's outbound web-service calls, generated data structures and their fields, calls into "
+ "NatJav programs (item 214), each "
+ "with its COUNTERPART_OF twin in a counterpart project (project setting `counterparts`); "
+ "`unmatched=true` lists only those without a twin - what is not served or mirrored yet.")
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = Counterpart.class)))
@@ -852,21 +958,25 @@ public class AnalysisResource {
public Uni<Response> counterparts(@PathParam("project") String project,
@Parameter(description = "Narrow to one declaring module (identity or short name).")
@QueryParam("module") @Nullable String module,
@Parameter(description = "rest | dto | field; all when absent.")
@Parameter(description = "rest | dto | field | natjav | soap; all when absent.")
@QueryParam("kind") @Nullable String kind,
@Parameter(description = "Only rows without a counterpart.")
@QueryParam("unmatched") @Nullable Boolean unmatched,
@Parameter(description = "Item 214: only rows whose twin is in this module of the counterpart project.")
@QueryParam("counterpartModule") @Nullable String counterpartModule,
@QueryParam("countOnly") @Nullable Boolean countOnly,
@QueryParam("limit") @Nullable Integer limit,
@QueryParam("offset") @Nullable Integer offset) {
String effKind = kind == null || kind.isBlank() ? null : kind.strip().toLowerCase(java.util.Locale.ROOT);
if (effKind != null && !List.of("rest", "dto", "field").contains(effKind)) {
if (effKind != null && !List.of("rest", "dto", "field", "natjav", "soap").contains(effKind)) {
return Uni.createFrom().item(ProjectResource.error(Response.Status.BAD_REQUEST, "KIND_UNSUPPORTED",
"Unknown kind '" + kind + "'; expected rest, dto or field"));
"Unknown kind '" + kind + "'; expected rest, dto, field, natjav or soap"));
}
int effLimit = isCountOnly(countOnly) ? 1 : uncappedLimit(limit);
return withProject(project, () -> graphRepository.counterpartsPage(project, module, effKind,
Boolean.TRUE.equals(unmatched), effLimit, effectiveOffset(offset))
Boolean.TRUE.equals(unmatched),
counterpartModule == null || counterpartModule.isBlank() ? null : counterpartModule.strip(),
effLimit, effectiveOffset(offset))
.map(page -> isCountOnly(countOnly) ? countOnly(page) : paged(page)));
}
@@ -909,13 +1019,14 @@ public class AnalysisResource {
@Operation(summary = "The style inventory: sx/style/styled blocks and CSS rules (item 196)",
description = "One row per style block with its CSS keys (`properties`, nested selectors flattened), hard-coded "
+ "colours/lengths (`literals`) and the theme tokens it reads. `withLiterals=true` = only blocks that bypass "
+ "the theme; `kind` = sx | style | styled | css; `module` = one component module.")
+ "the theme; `kind` = sx | style | styled | css | scss (item 233: nested SCSS rules) | template (item 233: "
+ "inline styles of an Angular template); `module` = one component module.")
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = StyleBlock.class)))
@APIResponse(responseCode = "400", description = "Unknown 'kind'.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public Uni<Response> styles(@PathParam("project") String project,
@QueryParam("module") @Nullable String module,
@Parameter(description = "sx | style | styled | css; all when absent.")
@Parameter(description = "sx | style | styled | css | scss | template; all when absent.")
@QueryParam("kind") @Nullable String kind,
@Parameter(description = "Only blocks with hard-coded colour/length literals.")
@QueryParam("withLiterals") @Nullable Boolean withLiterals,
@@ -924,9 +1035,9 @@ public class AnalysisResource {
@QueryParam("offset") @Nullable Integer offset) {
String trimmedKind = blankToNull(kind);
String effKind = trimmedKind == null ? null : trimmedKind.toLowerCase(java.util.Locale.ROOT);
if (effKind != null && !List.of("sx", "style", "styled", "css").contains(effKind)) {
if (effKind != null && !List.of("sx", "style", "styled", "css", "scss", "template").contains(effKind)) {
return Uni.createFrom().item(ProjectResource.error(Response.Status.BAD_REQUEST, "KIND_UNSUPPORTED",
"Unknown kind '" + kind + "'; expected sx, style, styled or css"));
"Unknown kind '" + kind + "'; expected sx, style, styled, css, scss or template"));
}
int effLimit = isCountOnly(countOnly) ? 1 : uncappedLimit(limit);
return withProject(project, () -> graphRepository.stylesPage(project, blankToNull(module), effKind,
@@ -1078,6 +1189,12 @@ public class AnalysisResource {
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public Uni<Response> searchAnnotation(@PathParam("project") String project, @QueryParam("name") @Nullable String name,
@QueryParam("type") @Nullable String type,
@Parameter(description = "Item 210: match the whole annotation name (case-sensitive) instead of a substring.")
@QueryParam("exact") @Nullable Boolean exact,
@Parameter(description = "Item 227: only where the matched annotation has this attribute.")
@QueryParam("attribute") @Nullable String attribute,
@Parameter(description = "Item 227: only where the matched annotation has an attribute of this value.")
@QueryParam("value") @Nullable String value,
@Parameter(description = "Return only {\"count\": n} instead of the rows (item 131).")
@QueryParam("countOnly") @Nullable Boolean countOnly,
@QueryParam("limit") @Nullable Integer limit, @QueryParam("offset") @Nullable Integer offset) {
@@ -1094,7 +1211,9 @@ public class AnalysisResource {
}
}
return withProject(project, () -> graphRepository.searchAnnotationPage(project, name,
type != null ? type.toUpperCase() : null, countAwareLimit(countOnly, limit), effectiveOffset(offset))
type != null ? type.toUpperCase() : null, Boolean.TRUE.equals(exact),
attribute == null || attribute.isBlank() ? null : attribute.strip(),
value == null || value.isEmpty() ? null : value, countAwareLimit(countOnly, limit), effectiveOffset(offset))
.map(page -> isCountOnly(countOnly) ? countOnly(page) : paged(page)));
}
@@ -1200,6 +1319,159 @@ public class AnalysisResource {
return withIngestedModule(project, name, sourceFile, resolvedName -> graphRepository.workfileAccesses(project, resolvedName, effLimit, effOffset, anySource(sourceFile)).map(this::ok));
}
@GET
@Path("/screens")
@Operation(summary = "Screens of an Angular front end (item 230)",
description = "Routes (path, component, title, guards) and dialogs (component, who opens it).")
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = Screen.class)))
@APIResponse(responseCode = "400", description = "Unknown kind.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public Uni<Response> screens(@PathParam("project") String project,
@Parameter(description = "route | dialog; both when absent.")
@QueryParam("kind") @Nullable String kind,
@Parameter(description = "Only screens showing this component (module, file name or class).")
@QueryParam("component") @Nullable String component,
@QueryParam("countOnly") @Nullable Boolean countOnly,
@QueryParam("limit") @Nullable Integer limit,
@QueryParam("offset") @Nullable Integer offset) {
String effKind = kind == null || kind.isBlank() ? null : kind.strip().toLowerCase(java.util.Locale.ROOT);
if (effKind != null && !List.of("route", "dialog").contains(effKind)) {
return Uni.createFrom().item(ProjectResource.error(Response.Status.BAD_REQUEST, "KIND_UNSUPPORTED",
"Unknown kind '" + kind + "'; expected route or dialog"));
}
int effLimit = isCountOnly(countOnly) ? 1 : uncappedLimit(limit);
return withProject(project, () -> graphRepository.screens(project, effKind,
component == null || component.isBlank() ? null : component.strip(), effLimit, effectiveOffset(offset))
.map(page -> isCountOnly(countOnly) ? countOnly(page) : paged(page)));
}
@GET
@Path("/soap-endpoints")
@Operation(summary = "SOAP endpoints (item 225)",
description = "Axis2 services (services.xml), WSDL contracts and JAX-WS @WebService classes, one row per operation, "
+ "with the COUNTERPART_OF {via: soap} twin in a counterpart project.")
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = SoapEndpoint.class)))
@APIResponse(responseCode = "400", description = "Unknown kind.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public Uni<Response> soapEndpoints(@PathParam("project") String project,
@QueryParam("service") @Nullable String service,
@Parameter(description = "axis2 | wsdl | jaxws; all when absent.")
@QueryParam("kind") @Nullable String kind,
@Parameter(description = "Item 238: only services exposing this EntireX port (WSK369), case-insensitive.")
@QueryParam("port") @Nullable String port,
@QueryParam("countOnly") @Nullable Boolean countOnly,
@QueryParam("limit") @Nullable Integer limit,
@QueryParam("offset") @Nullable Integer offset) {
String effKind = kind == null || kind.isBlank() ? null : kind.strip().toLowerCase(java.util.Locale.ROOT);
if (effKind != null && !List.of("axis2", "wsdl", "jaxws").contains(effKind)) {
return Uni.createFrom().item(ProjectResource.error(Response.Status.BAD_REQUEST, "KIND_UNSUPPORTED",
"Unknown kind '" + kind + "'; expected axis2, wsdl or jaxws"));
}
int effLimit = isCountOnly(countOnly) ? 1 : uncappedLimit(limit);
return withProject(project, () -> graphRepository.soapEndpoints(project,
service == null || service.isBlank() ? null : service.strip(), effKind,
port == null || port.isBlank() ? null : port.strip(), effLimit, effectiveOffset(offset))
.map(page -> isCountOnly(countOnly) ? countOnly(page) : paged(page)));
}
@GET
@Path("/db-tables")
@Operation(summary = "The project's tables (item 219)",
description = "One row per table with the modules mapping it; `stats=true` adds reading/writing modules and statement counts.")
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = DbTableInfo.class)))
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public Uni<Response> dbTables(@PathParam("project") String project,
@Parameter(description = "Case-insensitive fragment of the table name.")
@QueryParam("name") @Nullable String name,
@QueryParam("stats") @Nullable Boolean stats,
@QueryParam("countOnly") @Nullable Boolean countOnly,
@QueryParam("limit") @Nullable Integer limit,
@QueryParam("offset") @Nullable Integer offset) {
int effLimit = isCountOnly(countOnly) ? 1 : uncappedLimit(limit);
return withProject(project, () -> graphRepository.dbTables(project, name == null || name.isBlank() ? null : name.strip(),
Boolean.TRUE.equals(stats), effLimit, effectiveOffset(offset))
.map(page -> isCountOnly(countOnly) ? countOnly(page) : paged(page)));
}
@GET
@Path("/call-graph/aggregate")
@Operation(summary = "The module call graph rolled up by directory (item 218)",
description = "CALLS_MODULE pairs grouped by the first `level` directory segments of caller and callee.")
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = CallGraphGroup.class)))
@APIResponse(responseCode = "400", description = "Unsupported groupBy.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public Uni<Response> callGraphAggregate(@PathParam("project") String project,
@Parameter(description = "`path` (the only grouping so far).")
@QueryParam("groupBy") @Nullable String groupBy,
@Parameter(description = "Directory segments that form a group (default 1).")
@QueryParam("level") @Nullable Integer level,
@QueryParam("countOnly") @Nullable Boolean countOnly,
@QueryParam("limit") @Nullable Integer limit,
@QueryParam("offset") @Nullable Integer offset) {
if (groupBy != null && !groupBy.isBlank() && !groupBy.strip().equalsIgnoreCase("path")) {
return Uni.createFrom().item(ProjectResource.error(Response.Status.BAD_REQUEST, "GROUP_BY_UNSUPPORTED",
"Unknown groupBy '" + groupBy + "'; expected path"));
}
int effLimit = isCountOnly(countOnly) ? 1 : uncappedLimit(limit);
return withProject(project, () -> graphRepository.callGraphAggregate(project, level == null ? 1 : level,
effLimit, effectiveOffset(offset))
.map(page -> isCountOnly(countOnly) ? countOnly(page) : paged(page)));
}
@GET
@Path("/modules/{name}/transactions")
@Operation(summary = "Transaction boundaries of a module (item 216)",
description = "END TRANSACTION / BACKOUT TRANSACTION sites of the module and, with `depth`, of every module "
+ "within that many module hops of it.")
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = StatementSite.class)))
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public Uni<Response> transactions(@PathParam("project") String project, @PathParam("name") String name,
@Parameter(description = "Module hops to follow (0 = the module only).")
@QueryParam("depth") @Nullable Integer depth,
@QueryParam("sourceFile") @Nullable String sourceFile) {
return withIngestedModule(project, name, sourceFile, resolvedName ->
graphRepository.transactions(project, resolvedName, depth == null ? 0 : depth).map(this::ok));
}
/**
* Items 216/221: the statement kinds recorded as {@code STATEMENT} nodes.
*/
private static final List<String> STATEMENT_KINDS = List.of(
"END_TRANSACTION", "BACKOUT_TRANSACTION", "REINPUT", "SET_KEY", "STACK_DATA", "ON_ERROR");
@GET
@Path("/search/statement")
@Operation(summary = "Find statements by kind (item 221)",
description = "Sites of END_TRANSACTION, BACKOUT_TRANSACTION, REINPUT, SET_KEY, STACK_DATA or ON_ERROR, "
+ "optionally several kinds (comma-separated) and one module.")
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = StatementSite.class)))
@APIResponse(responseCode = "400", description = "Unknown kind.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public Uni<Response> searchStatement(@PathParam("project") String project,
@QueryParam("kind") @Nullable String kind,
@QueryParam("module") @Nullable String module,
@QueryParam("countOnly") @Nullable Boolean countOnly,
@QueryParam("limit") @Nullable Integer limit,
@QueryParam("offset") @Nullable Integer offset) {
List<String> kinds = null;
if (kind != null && !kind.isBlank()) {
kinds = java.util.Arrays.stream(kind.split(",")).map(String::strip).filter(k -> !k.isEmpty())
.map(k -> k.toUpperCase(java.util.Locale.ROOT).replace(' ', '_')).toList();
for (String k : kinds) {
if (!STATEMENT_KINDS.contains(k)) {
return Uni.createFrom().item(ProjectResource.error(Response.Status.BAD_REQUEST, "KIND_UNSUPPORTED",
"Unknown kind '" + k + "'; expected one of " + String.join(", ", STATEMENT_KINDS)));
}
}
}
List<String> modules = module == null || module.isBlank() ? null : List.of(module.strip());
List<String> effKinds = kinds;
int effLimit = isCountOnly(countOnly) ? 1 : uncappedLimit(limit);
return withProject(project, () -> graphRepository.statementSites(project, effKinds, modules, effLimit,
effectiveOffset(offset))
.map(page -> isCountOnly(countOnly) ? countOnly(page) : paged(page)));
}
@GET
@Path("/source")
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = SourceSnippet.class)))

View File

@@ -64,7 +64,7 @@ public class ProjectResource {
.build();
}
private static final List<String> SUPPORTED_LANGUAGES = List.of("natural", "java", "typescript");
private static final List<String> SUPPORTED_LANGUAGES = List.of("natural", "java", "natjav", "typescript", "angular");
private static @Nullable String normalize(@Nullable String value) {
return value == null || value.isBlank() ? null : value.strip();

View File

@@ -11,6 +11,10 @@ import com.agenticcode.parsercore.ast.spi.LineCounter;
import com.agenticcode.parserjava.JavaCoarseScanner;
import com.agenticcode.parserjava.JavaLineCounter;
import com.agenticcode.parserjava.JavaParser;
import com.agenticcode.parsernatjav.NatJavCoarseScanner;
import com.agenticcode.parsernatjav.NatJavDdmResolver;
import com.agenticcode.parsernatjav.NatJavLineCounter;
import com.agenticcode.parsernatjav.NatJavParser;
import com.agenticcode.parsernatural.CopycodeResolver;
import com.agenticcode.parsernatural.NaturalCoarseScanner;
import com.agenticcode.parsernatural.NaturalLineCounter;
@@ -43,7 +47,11 @@ public class AstIngestService {
private final CoarseScanner javaScanner = new JavaCoarseScanner();
private final NaturalCoarseScanner naturalScanner = new NaturalCoarseScanner();
private final LineCounter javaLineCounter = new JavaLineCounter();
private final LineCounter configLineCounter = new com.agenticcode.parserjava.ConfigLineCounter();
private final LineCounter naturalLineCounter = new NaturalLineCounter();
private final NatJavParser natJavParser = new NatJavParser();
private final NatJavCoarseScanner natJavScanner = new NatJavCoarseScanner();
private final LineCounter natJavLineCounter = new NatJavLineCounter();
// Item 192: one parser for .ts/.tsx/.css; the per-ingest TypeScriptProject carries workspaces,
// package names and (Tier-2) the sidecar facts, like CopycodeResolver does for Natural.
private final TypeScriptParser typeScriptParser = new TypeScriptParser();
@@ -121,20 +129,25 @@ public class AstIngestService {
*/
public LanguageParser.ParseResult parse(SourceFiles.Language language, String sourceFile, String content,
CopycodeResolver copycodes) {
return parse(language, sourceFile, content, copycodes, TypeScriptProject.NONE);
return parse(language, sourceFile, content, copycodes, NatJavDdmResolver.NONE, TypeScriptProject.NONE);
}
/**
* Parses {@code content} with the parser for {@code language}; {@code copycodes} serves Natural
* (item 46a), {@code typescript} serves TypeScript/CSS (item 192). The switch is exhaustive on
* purpose: a new {@link SourceFiles.Language} must be routed here, not fall through to a default.
* Parses {@code content} with the parser for {@code language}. Each language-specific context is
* ignored by the others: {@code copycodes} serves Natural (item 46a), {@code ddms} the NatJav DDM
* index (item D, so a view resolves to its table and columns), {@code typescript} TypeScript/CSS
* (item 192). The switch is exhaustive on purpose: a language added without a parser must fail to
* compile, not silently hand its files to another parser and store a plausible-looking but wrong
* graph.
*/
public LanguageParser.ParseResult parse(SourceFiles.Language language, String sourceFile, String content,
CopycodeResolver copycodes, TypeScriptProject typescript) {
CopycodeResolver copycodes, NatJavDdmResolver ddms, TypeScriptProject typescript) {
return switch (language) {
case JAVA -> javaParser.parse(sourceFile, content);
case NATJAV -> natJavParser.parse(sourceFile, content, ddms);
case NATURAL -> naturalParser.parse(sourceFile, content, copycodes);
case TYPESCRIPT, CSS -> typeScriptParser.parse(sourceFile, content, typescript);
case CONFIG -> parseConfig(sourceFile, content);
};
}
@@ -153,18 +166,46 @@ public class AstIngestService {
*/
public LanguageParser.ParseResult coarseScan(SourceFiles.Language language, String sourceFile, String content,
CopycodeResolver copycodes) {
return coarseScan(language, sourceFile, content, copycodes, TypeScriptProject.NONE);
return coarseScan(language, sourceFile, content, copycodes, NatJavDdmResolver.NONE, TypeScriptProject.NONE);
}
/**
* As {@link #parse(SourceFiles.Language, String, String, CopycodeResolver, NatJavDdmResolver, TypeScriptProject)},
* for the Tier-1 outline scan.
*/
public LanguageParser.ParseResult coarseScan(SourceFiles.Language language, String sourceFile, String content,
CopycodeResolver copycodes, TypeScriptProject typescript) {
CopycodeResolver copycodes, NatJavDdmResolver ddms, TypeScriptProject typescript) {
return switch (language) {
case JAVA -> javaScanner.scan(sourceFile, content);
case NATJAV -> natJavScanner.scan(sourceFile, content, ddms);
case NATURAL -> naturalScanner.scan(sourceFile, content, copycodes);
case TYPESCRIPT, CSS -> typeScriptScanner.scan(sourceFile, content, typescript);
// A descriptor is its own outline: the whole parse is cheap and there is no deeper tier.
case CONFIG -> parseConfig(sourceFile, content);
};
}
/**
* Items 225/226: a SOAP descriptor (WSDL, Axis2 {@code services.xml}) yields services and operations;
* every other configuration file its entries.
*/
private static LanguageParser.ParseResult parseConfig(String sourceFile, String content) {
String name = java.nio.file.Path.of(sourceFile).getFileName().toString().toLowerCase(java.util.Locale.ROOT);
if (name.endsWith(".wsdl") || name.equals("services.xml")) {
return com.agenticcode.parserjava.SoapDescriptorParser.parse(sourceFile, content);
}
// Item 239: SourceFiles admits a *binding*.xml only when it is a JAX-WS wsimport binding.
if (name.endsWith(".xml") && name.contains("binding") && content.contains("wsdlLocation")
&& content.contains("/xml/ns/jaxws")) {
return com.agenticcode.parserjava.SoapDescriptorParser.parseJaxwsBinding(sourceFile, content);
}
// Item 253: SourceFiles admits a .yaml/.yml/.json only when it is an OpenAPI/Swagger document.
if (name.endsWith(".yaml") || name.endsWith(".yml") || name.endsWith(".json")) {
return com.agenticcode.parserjava.OpenApiSpecParser.parse(sourceFile, content);
}
return com.agenticcode.parserjava.ConfigFileParser.parse(sourceFile, content);
}
/**
* Item 44: deterministic per-language LoC/SLoC of {@code content}. Uses the same {@link LineCounter}
* the Tier-1 coarse scanners use, so a module's metrics are identical at any ingest depth.
@@ -172,9 +213,11 @@ public class AstIngestService {
public LocMetrics count(SourceFiles.Language language, String content) {
LineCounter counter = switch (language) {
case JAVA -> javaLineCounter;
case NATJAV -> natJavLineCounter;
case NATURAL -> naturalLineCounter;
case TYPESCRIPT -> typeScriptLineCounter;
case CSS -> cssLineCounter;
case CONFIG -> configLineCounter;
};
return counter.count(content);
}

View File

@@ -224,8 +224,19 @@ public class DeepIngestCoordinator {
* coarse one already produced.
*/
private static boolean isWarmable(String sourceFile) {
// Item 205: a placeholder module has sourceFile "" and every fan-out result carries it (the
// call-tree file index, callers of an unresolved callee). It is not a file: resolved against
// the project root it names the root directory itself, so the warm re-scanned the project up
// to the fan-out budget on every request (k3: 40 s per call-tree) and never became FULL.
if (sourceFile.isBlank()) {
return false;
}
String lower = sourceFile.toLowerCase(Locale.ROOT);
return !lower.endsWith(".lda") && !lower.endsWith(".pda") && !lower.endsWith(".gda");
// Item 224: a NatJav data area (`YPDSGVT_DA.java`) is the same case as a .lda — a DATA_STRUCTURE
// shell that is never marked FULL — so it was re-warmed on every call that surfaced it, each time
// with a scoped finalize: search/identifier?name=YPDSGVT took 148 s on korin with a 2 s query.
return !lower.endsWith(".lda") && !lower.endsWith(".pda") && !lower.endsWith(".gda")
&& !lower.endsWith("_da.java");
}
/**

View File

@@ -0,0 +1,146 @@
package com.agenticcode.codeserver.service;
import com.agenticcode.parsernatjav.NatJavDdm;
import com.agenticcode.parsernatjav.NatJavDdmResolver;
import org.jboss.logging.Logger;
import org.jspecify.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A NatJav project's data definition modules, backing {@link NatJavDdmResolver}. Built once per
* ingest from the project's {@code *_DDM.java} files (243 of them in the reference corpus), so a
* view can be given the table and column names that live in the DDM rather than in the view's own
* declaration.
*
* <p>The same split as {@link CopycodeLibrary} on the Natural side: the parser SPI stays
* file-agnostic and the ingest layer, which has filesystem access, supplies the index. Unlike
* copycodes these files <em>are</em> ingested in their own right — a DDM is a module with its
* columns — so this index is a second, cheap read of files the walk already knows about.
*/
final class NatJavDdmLibrary implements NatJavDdmResolver {
private static final Logger LOG = Logger.getLogger(NatJavDdmLibrary.class);
/**
* Filename suffix of a generated DDM class. Every {@code NaturalDdm} subclass in the corpus
* carries it and nothing else does, so the scan need not read non-DDM files at all.
*/
private static final String SUFFIX = "_DDM.java";
/**
* Filename suffix of a generated data area, whose {@code @ViewOf} declarations are indexed so a
* program using the area can resolve a statement's operand to a table.
*/
private static final String AREA_SUFFIX = "_DA.java";
/**
* {@code @ViewOf <ddm> <field> = view(...)} inside a data area — read off the text rather than
* parsed, because only two identifiers are needed and the area may hold thousands of fields.
*/
private static final java.util.regex.Pattern AREA_VIEW = java.util.regex.Pattern.compile(
"@ViewOf\\s+([\\w.]+)\\s+([\\w$]+)\\s*=\\s*view\\s*\\(");
/**
* A data area's field declaration, reduced to what item E1 needs: the Java identifier and the
* Natural name literal the transpiler preserved. Read off the text rather than parsed — the
* corpus holds 739,346 of these and only two identifiers per line are wanted.
*/
private static final java.util.regex.Pattern AREA_FIELD = java.util.regex.Pattern.compile(
"@L\\d\\s+(?:@\\w+\\s+[\\w.]+\\s+|Group\\s+|[A-Z]\\s+)?([\\w$]+)\\s*=\\s*[A-Za-z]+\\("
+ "\\s*(?:\\d+\\s*(?:,\\s*\\d+\\s*)?,\\s*)?\"([^\"]+)\"");
private final Map<String, Ddm> byClassName;
/**
* {@code <areaClass>#<viewField>} → the DDM class that view reads.
*/
private final Map<String, String> areaViews;
/**
* {@code <areaClass>#<javaFieldName>} → the field's Natural name.
*/
private final Map<String, String> areaFields;
private NatJavDdmLibrary(Map<String, Ddm> byClassName, Map<String, String> areaViews,
Map<String, String> areaFields) {
this.byClassName = byClassName;
this.areaViews = areaViews;
this.areaFields = areaFields;
}
private static String areaViewKey(String areaClassName, String viewName) {
return areaClassName.toUpperCase(java.util.Locale.ROOT) + "#" + viewName;
}
/**
* @param candidates the project's ingestible files
* @return the index, or {@link NatJavDdmResolver#NONE} when the project holds no DDM
*/
static NatJavDdmResolver scan(List<ProjectIngestService.Candidate> candidates) {
Map<String, Ddm> byClassName = new HashMap<>();
Map<String, String> areaViews = new HashMap<>();
Map<String, String> areaFields = new HashMap<>();
for (ProjectIngestService.Candidate candidate : candidates) {
if (candidate.kind().language() != SourceFiles.Language.NATJAV) {
continue;
}
String fileName = candidate.file().getFileName().toString();
String className = SourceFiles.stem(candidate.file());
try {
if (fileName.endsWith(SUFFIX)) {
Ddm ddm = NatJavDdm.parse(SourceFiles.read(candidate.file()), className);
if (ddm != null) {
byClassName.put(className, ddm);
}
} else if (fileName.endsWith(AREA_SUFFIX)) {
String text = SourceFiles.read(candidate.file());
java.util.regex.Matcher matcher = AREA_VIEW.matcher(text);
while (matcher.find()) {
String ddmClass = matcher.group(1);
int dot = ddmClass.lastIndexOf('.');
areaViews.put(areaViewKey(className, matcher.group(2)),
dot >= 0 ? ddmClass.substring(dot + 1) : ddmClass);
}
java.util.regex.Matcher fields = AREA_FIELD.matcher(text);
while (fields.find()) {
areaFields.putIfAbsent(areaViewKey(className, fields.group(1)), fields.group(2));
}
}
} catch (IOException | RuntimeException e) {
// Unreadable or unparseable here is reported as a failure when the file is ingested;
// the views referring to it simply resolve no columns.
LOG.debugf("NatJav DDM '%s' could not be indexed: %s", className, e.toString());
}
}
// All three, not just the DDMs: a project may hold data areas without a single *_DDM.java,
// and bailing on the DDM count alone threw away the areaViews/areaFields just computed —
// silently costing every cross-area dataflow edge, with no log line to say so.
if (byClassName.isEmpty() && areaViews.isEmpty() && areaFields.isEmpty()) {
return NatJavDdmResolver.NONE;
}
LOG.infof("NatJav: indexed %d DDMs, %d data-area views and %d data-area fields",
byClassName.size(), areaViews.size(), areaFields.size());
return new NatJavDdmLibrary(byClassName, areaViews, areaFields);
}
@Override
public @Nullable Ddm resolve(String ddmClassName) {
return byClassName.get(ddmClassName.toUpperCase(java.util.Locale.ROOT));
}
@Override
public @Nullable String resolveAreaField(String areaClassName, String javaFieldName) {
return areaFields.get(areaViewKey(areaClassName, javaFieldName));
}
@Override
public @Nullable Ddm resolveAreaView(String areaClassName, String viewName) {
String ddmClass = areaViews.get(areaViewKey(areaClassName, viewName));
return ddmClass == null ? null : resolve(ddmClass);
}
}

View File

@@ -0,0 +1,248 @@
package com.agenticcode.codeserver.service;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import org.jboss.logging.Logger;
import org.jspecify.annotations.Nullable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Disambiguates NatJav module names that two files claim, before the ingest either merges them into
* one ambiguous node or drops both as duplicates.
*
* <p>A NatJav module's identity is the Natural name from its {@code @Natural(moduleName=...)}, which
* is right for the other 22,790 files of the reference corpus but leaves two collision classes:
*
* <ul>
* <li><b>Library homonyms.</b> Natural module names are unique <em>per library</em>, not globally,
* so {@code KORIN} and {@code ABFRAGE} may each hold an {@code ACOMMON} (and each its own GDA
* {@code COMMON}). Both are real, different modules. They are qualified with the library —
* the top-level package — as {@code KORIN.ACOMMON}, and only where the collision actually
* occurs, so the other 22,790 modules stay addressable by their bare Natural name.</li>
* <li><b>A copied header the migration did not update.</b> {@code ZB80BM6.java} declares
* {@code moduleName="ZB80BM5"} — provably wrong: {@code ZB80BM5.java} exists, and the two files
* differ in 178 of ~570 lines. Three module pairs and three data-area pairs in the corpus are
* like this. Here the class name is the trustworthy half, so the file whose class name matches
* the declared name keeps it and the impostor falls back to its own.</li>
* </ul>
*
* <p>The distinction between the two is which file <em>owns</em> the name: the one whose class name
* (less the transpiler's {@code _DA}/{@code _DDM} suffix) is the declared name. Exactly one owner
* means the others are impostors; none or several means the declaration is not in dispute and the
* files differ by library instead.
*
* <p>This has to be a cross-file pass — a parser looking at one file cannot know a name is contested
* — so it reads only each file's head, which is where the annotation and the class declaration are.
*/
final class NatJavIdentities {
private static final Logger LOG = Logger.getLogger(NatJavIdentities.class);
/**
* Property recording the {@code moduleName} a file declared but did not get, so the contradiction
* stays visible in the graph instead of being silently corrected away.
*/
static final String NAME_CONFLICT = "naturalNameConflict";
/**
* Bytes of each file read to find the annotation. The generated header (licence comment, package,
* import, Javadoc, annotation, class declaration) is well under this in every corpus file.
*/
private static final int HEAD_BYTES = 8192;
private static final Pattern MODULE_NAME =
Pattern.compile("@Natural\\s*\\(\\s*moduleName\\s*=\\s*\"([^\"]+)\"");
private NatJavIdentities() {
}
/**
* Builds the rename map for a NatJav project.
*
* @param root the project root
* @param candidates every ingestible file of the project (the whole walk, so a collision is seen
* even when only one of the two files is being ingested)
* @return project-relative source file → the name its module node must carry instead of the one
* it declares. Empty for a project with no contested name, and for every non-NatJav project.
*/
static Map<String, Rename> scan(Path root, List<ProjectIngestService.Candidate> candidates) {
Map<String, List<Claim>> byDeclaredName = new LinkedHashMap<>();
for (ProjectIngestService.Candidate candidate : candidates) {
if (candidate.kind().language() != SourceFiles.Language.NATJAV) {
continue;
}
Claim claim = claimOf(root, candidate.file());
byDeclaredName.computeIfAbsent(claim.declaredName(), n -> new ArrayList<>()).add(claim);
}
Map<String, Rename> renames = new HashMap<>();
byDeclaredName.forEach((declaredName, claims) -> {
if (claims.size() < 2) {
return;
}
List<Claim> owners = claims.stream().filter(c -> c.ownsDeclaredName(declaredName)).toList();
if (owners.size() == 1) {
for (Claim claim : claims) {
if (claim != owners.get(0)) {
renames.put(claim.sourceFile(), new Rename(claim.baseName(), declaredName));
LOG.infof("NatJav: '%s' also declares moduleName '%s', which %s owns — ingesting it as '%s'",
claim.sourceFile(), declaredName, owners.get(0).sourceFile(), claim.baseName());
}
}
return;
}
// Not a contested declaration: the files agree, they just live in different libraries.
long libraries = claims.stream().map(Claim::library).distinct().count();
// A file directly in the project root has no library, and qualifying it would name the
// module ".ACOMMON" — an identity every later query would have to be handed verbatim.
boolean allNamed = claims.stream().noneMatch(c -> c.library().isEmpty());
if (allNamed && libraries == claims.size()) {
for (Claim claim : claims) {
renames.put(claim.sourceFile(), new Rename(claim.library() + "." + declaredName, null));
}
}
// Same library and no single owner: nothing here distinguishes the files, so they are left
// to the duplicate detection, which reports them rather than binding one arbitrarily.
});
return renames;
}
/**
* @return every name a NatJav file's module answers to besides its filename stem: the Natural
* name from its {@code @Natural(moduleName=...)}, and the renamed identity where this pass gave
* it one. The by-name ingest indexes candidates by stem — the <em>Java class</em> name — while
* the graph exposes the Natural name, so without this a targeted ingest of {@code V-MIT04} or
* {@code WSAIVP} reports "unresolved" for a module the graph carries under exactly that name.
*/
static Map<String, String> graphNames(Path root, List<ProjectIngestService.Candidate> candidates,
Map<String, Rename> renames) {
Map<String, String> byFile = new HashMap<>();
for (ProjectIngestService.Candidate candidate : candidates) {
if (candidate.kind().language() != SourceFiles.Language.NATJAV) {
continue;
}
Claim claim = claimOf(root, candidate.file());
Rename rename = renames.get(claim.sourceFile());
byFile.put(claim.sourceFile(), rename != null ? rename.name() : claim.declaredName());
}
return byFile;
}
/**
* Applies {@code renames} to the module/data-structure shell of one parse result.
*
* @return {@code result} unchanged when this file is not renamed
*/
static ParseResult apply(ParseResult result, String sourceFile, Map<String, Rename> renames) {
Rename rename = renames.get(sourceFile);
if (rename == null) {
return result;
}
List<AstNode> nodes = new ArrayList<>(result.nodes().size());
for (AstNode node : result.nodes()) {
nodes.add(isShell(node, sourceFile) ? renamed(node, rename) : node);
}
return new ParseResult(nodes, result.edges());
}
/**
* @return whether {@code node} is the file's own shell — the node standing for the file, which is
* the only one whose name this pass may change. Identified the way the parser builds it: it
* carries the file's path, starts at line 1 and holds the {@code className} property.
*/
private static boolean isShell(AstNode node, String sourceFile) {
return (node.type() == NodeType.MODULE || node.type() == NodeType.DATA_STRUCTURE)
&& node.sourceFile().equals(sourceFile)
&& node.startLine() == 1
&& node.properties() != null
&& node.properties().containsKey("className");
}
private static AstNode renamed(AstNode node, Rename rename) {
Map<String, String> properties = new LinkedHashMap<>();
if (node.properties() != null) {
properties.putAll(node.properties());
}
if (rename.conflictingName() != null) {
properties.put(NAME_CONFLICT, rename.conflictingName());
}
return new AstNode(node.id(), node.type(), rename.name(), node.sourceFile(), node.language(),
node.startLine(), node.endLine(), node.dataType(), node.value(), properties);
}
private static Claim claimOf(Path root, Path file) {
String className = SourceFiles.stem(file);
String head = head(file);
Matcher matcher = MODULE_NAME.matcher(head);
String declaredName = matcher.find() ? matcher.group(1) : className;
Path relative = root.relativize(file);
String library = relative.getNameCount() > 1 ? relative.getName(0).toString() : "";
return new Claim(relative.toString().replace('\\', '/'), className, declaredName, library);
}
private static String head(Path file) {
try (var stream = Files.newInputStream(file)) {
return new String(stream.readNBytes(HEAD_BYTES), StandardCharsets.ISO_8859_1);
} catch (IOException e) {
// Unreadable here means unreadable at parse time too, where it is reported as a failure.
return "";
}
}
/**
* One file's claim to a module name.
*
* @param sourceFile the project-relative path
* @param className the Java class name (the filename stem, upper-cased by {@link SourceFiles})
* @param declaredName the {@code @Natural(moduleName=...)}, or the class name if it declares none
* @param library the top-level directory, i.e. the Natural library
*/
private record Claim(String sourceFile, String className, String declaredName, String library) {
/**
* @return the class name without the transpiler's data-area / DDM suffix — what the class name
* says the Natural name is
*/
String baseName() {
for (String suffix : List.of("_DA", "_DDM")) {
if (className.endsWith(suffix)) {
return className.substring(0, className.length() - suffix.length());
}
}
return className;
}
/**
* @return whether this file is the natural owner of {@code declaredName}: its own class name
* says so, independently of the annotation that is in dispute
*/
boolean ownsDeclaredName(String name) {
return baseName().equalsIgnoreCase(name.toUpperCase(Locale.ROOT))
|| baseName().equals(name);
}
}
/**
* The name a file's module node must carry instead of the one it declares.
*
* @param name the replacement name
* @param conflictingName the declared name that was rejected because another file owns it, or
* {@code null} when the rename is a library qualification rather than a
* correction
*/
record Rename(String name, @Nullable String conflictingName) {
}
}

View File

@@ -2,10 +2,12 @@ package com.agenticcode.codeserver.service;
import com.agenticcode.neo4jstore.graph.EnrichmentLevel;
import com.agenticcode.neo4jstore.graph.IngestDepth;
import com.agenticcode.neo4jstore.graph.IngestProgress;
import com.agenticcode.neo4jstore.graph.ProjectInfo;
import com.agenticcode.neo4jstore.graph.ProjectIngestInfo;
import com.agenticcode.parsercore.ast.model.*;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import com.agenticcode.parsernatjav.NatJavDdmResolver;
import com.agenticcode.parsertypescript.TypeScriptProject;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@@ -55,11 +57,14 @@ public class ProjectIngestService {
private final ProjectMetadataCache projectMetadata;
// Item 192: builds the per-ingest TypeScript context (workspaces, packages, sidecar facts).
private final TypeScriptSidecarService typescript;
// Item 242: the phase line an asynchronous refresh job reports.
private final IngestProgress progress;
public ProjectIngestService(AstIngestService astIngestService,
VersionInfo versionInfo,
ProjectMetadataCache projectMetadata,
TypeScriptSidecarService typescript,
IngestProgress progress,
@ConfigProperty(name = "agenticcode.ingest.batch-size", defaultValue = "200") int batchSize,
@ConfigProperty(name = "agenticcode.deep-ingest.default-depth", defaultValue = "5") int defaultDeepDepth,
@ConfigProperty(name = "agenticcode.deep-ingest.max-depth", defaultValue = "20") int maxDeepDepth,
@@ -69,6 +74,7 @@ public class ProjectIngestService {
this.versionInfo = versionInfo;
this.projectMetadata = projectMetadata;
this.typescript = typescript;
this.progress = progress;
this.batchSize = Math.max(1, batchSize);
this.maxDeepDepth = Math.max(1, maxDeepDepth);
this.defaultDeepDepth = Math.min(Math.max(1, defaultDeepDepth), this.maxDeepDepth);
@@ -193,9 +199,20 @@ public class ProjectIngestService {
*/
private static List<Identity> fileIdentities(Parsed p) {
if (p.candidate().kind().kind() == NodeType.DATA_STRUCTURE) {
// The stem is the data area's identity for Natural (the file IS named after the area) but
// not for NatJav, whose file is named after the Java class: `WSK386P_DA.java` holds the
// area `WSK385P`. Keying on the stem there checked a name the graph does not use, so two
// areas colliding under their real name passed the guard and landed as one ambiguous
// node — WSK385P, WSSC31P and YLRY401A each did exactly that.
if (p.candidate().kind().language() == SourceFiles.Language.NATJAV) {
return natJavShellIdentities(p);
}
String stem = SourceFiles.stem(p.candidate().file());
return List.of(new Identity("DATA_STRUCTURE " + stem, stem, NodeType.DATA_STRUCTURE));
}
if (p.candidate().kind().language() == SourceFiles.Language.NATJAV) {
return natJavShellIdentities(p);
}
return p.result().nodes().stream()
.filter(node -> node.type() == NodeType.MODULE && !node.sourceFile().isEmpty())
.map(node -> identity(node, p.candidate().kind().language()))
@@ -203,20 +220,47 @@ public class ProjectIngestService {
.toList();
}
/**
* @return the identity of a NatJav file, taken from the shell node the parser actually produced
* rather than re-deduced from the filename.
*
* <p>The two can disagree: {@code SourceFiles.classify} decides MODULE vs DATA_STRUCTURE from the
* {@code _DA} suffix while the parser decides it from the {@code extends} target. A {@code *_DA.java}
* that does not extend {@code NaturalData}, or a {@code NaturalData} class named otherwise, then
* matched neither branch's filter and contributed <b>no</b> identity at all — so a genuine name
* collision on that file went unreported.
*/
private static List<Identity> natJavShellIdentities(Parsed p) {
return p.result().nodes().stream()
.filter(node -> (node.type() == NodeType.MODULE || node.type() == NodeType.DATA_STRUCTURE)
&& !node.sourceFile().isEmpty()
&& node.startLine() == 1
&& node.properties() != null
&& node.properties().containsKey("className"))
.map(node -> new Identity(node.type().name() + " " + node.name(), node.name(), node.type()))
.distinct()
.toList();
}
/**
* Item 24: reads and parses one candidate file, applying the shell/user-exit metric enrichment.
* Pure per-file work (no shared mutable state), so it is safe to run on a virtual thread per file.
*/
private Parsed parseCandidate(Candidate candidate, Path root, boolean coarse,
CopycodeLibrary copycodes, TypeScriptProject ts, Map<String, LocMetrics> userExit,
ProjectInfo project) throws IOException {
String content = SourceFiles.read(candidate.file());
ProjectInfo project,
Map<String, NatJavIdentities.Rename> natJavRenames,
NatJavDdmResolver ddms) throws IOException {
String sourceFile = relativeSourceFile(root, candidate.file());
// Item 234: a virtual file (the in-memory OpenAPI client) has no text; its facts carry everything.
String content = ts.facts().virtualFiles().containsKey(sourceFile) && !Files.exists(candidate.file())
? "" : SourceFiles.read(candidate.file());
ParseResult result = coarse
? astIngestService.coarseScan(candidate.kind().language(), sourceFile, content, copycodes, ts)
: withShellMetrics(astIngestService.parse(candidate.kind().language(), sourceFile, content, copycodes, ts),
content, candidate.kind().language());
? astIngestService.coarseScan(candidate.kind().language(), sourceFile, content, copycodes, ddms, ts)
: withShellMetrics(astIngestService.parse(candidate.kind().language(), sourceFile, content,
copycodes, ddms, ts), content, candidate.kind().language());
result = withUserExitMetrics(result, project.generatedDir(), userExit);
result = NatJavIdentities.apply(result, sourceFile, natJavRenames);
return new Parsed(candidate, result);
}
@@ -261,7 +305,9 @@ public class ProjectIngestService {
if (SourceFiles.isExcluded(root, file, effectiveExcludes)) {
continue;
}
SourceFiles.Kind kind = SourceFiles.classify(file);
// NatJav: the project's declared language decides what a .java file is (item A);
// TypeScript/CSS are picked up only by a typescript project (item 192).
SourceFiles.Kind kind = SourceFiles.classify(file, projectLanguage);
if (kind != null && SourceFiles.ingestedBy(kind.language(), projectLanguage)) {
candidates.add(new Candidate(file, kind));
}
@@ -276,11 +322,40 @@ public class ProjectIngestService {
* name a by-name walk reports as a duplicate.
*/
private static Map<NameKey, List<Candidate>> nameIndex(List<Candidate> candidates) {
return nameIndex(candidates, Map.of());
}
/**
* As {@link #nameIndex(List)}, additionally indexing each NatJav file under the name its module
* carries in the graph. The stem is the Java class name ({@code V_MIT04.java}); the graph knows
* the module as {@code V-MIT04}, so a seed given the name every query uses would otherwise never
* resolve. Both keys point at the same candidate.
*
* @param graphNames project-relative source file → the module's graph name, from
* {@link NatJavIdentities#graphNames}
*/
private static Map<NameKey, List<Candidate>> nameIndex(List<Candidate> candidates,
Map<String, String> graphNames) {
Map<NameKey, List<Candidate>> index = new HashMap<>();
for (Candidate candidate : candidates) {
index.computeIfAbsent(new NameKey(candidate.kind().kind(), SourceFiles.stem(candidate.file())),
String stem = SourceFiles.stem(candidate.file());
index.computeIfAbsent(new NameKey(candidate.kind().kind(), stem),
k -> new ArrayList<>()).add(candidate);
}
graphNames.forEach((sourceFile, graphName) -> {
for (Candidate candidate : candidates) {
if (!candidate.file().toString().replace('\\', '/').endsWith(sourceFile)
|| graphName.equalsIgnoreCase(SourceFiles.stem(candidate.file()))) {
continue;
}
List<Candidate> under = index.computeIfAbsent(
new NameKey(candidate.kind().kind(), graphName.toUpperCase(Locale.ROOT)),
k -> new ArrayList<>());
if (!under.contains(candidate)) {
under.add(candidate);
}
}
});
return index;
}
@@ -395,7 +470,7 @@ public class ProjectIngestService {
long startedAt = System.nanoTime();
IngestSummary summary = ingestRoot(project, deep ? EnrichmentLevel.FULL : EnrichmentLevel.CALL_GRAPH,
false, changedOnly, profile);
sweepDeletedFileOrphans(project);
sweepDeletedFileOrphans(project, Set.copyOf(summary.examinedFiles()));
LOG.infof("Refresh finished: project='%s', mode=%s, files=%d, modules=%d, failed=%d, %d s",
project.name(), deep ? "deep" : "call-graph", summary.examinedFiles().size(),
summary.ingested(), summary.failed().size(),
@@ -403,21 +478,44 @@ public class ProjectIngestService {
return summary;
}
/**
* Item 234: the sidecar's virtual files — the OpenAPI client a workspace generates at build time and
* does not keep on disk — join the walk as TypeScript candidates, so each becomes a module like the
* file it stands for. A path the walk already has (the client was generated after all) is not added twice.
*/
private static List<Candidate> withVirtualFiles(List<Candidate> candidates, Path root, TypeScriptProject ts) {
if (ts.facts().virtualFiles().isEmpty()) {
return candidates;
}
Set<Path> walked = new HashSet<>();
candidates.forEach(c -> walked.add(c.file()));
List<Candidate> all = new ArrayList<>(candidates);
ts.facts().virtualFiles().keySet().stream().sorted().forEach(sourceFile -> {
Path file = root.resolve(sourceFile);
SourceFiles.Kind kind = SourceFiles.classify(file, "angular");
if (kind != null && walked.add(file)) {
all.add(new Candidate(file, kind));
}
});
return all;
}
/**
* Item 43: after a whole-project refresh has re-ingested the current files, removes nodes for files
* that were <em>deleted</em> from disk (a refresh MERGEs current files and per-file reconciles them,
* but never touches files it no longer walks). Reconciles against the filesystem rather than the
* walked-candidate list, so on-demand dependency files (PDAs/LDAs/copybooks) that exist on disk but
* aren't top-level candidates are never wrongly swept.
* aren't top-level candidates are never wrongly swept. A file the refresh itself just ingested is never
* swept either: the item-234 virtual files exist only in the sidecar's program.
*/
private void sweepDeletedFileOrphans(ProjectInfo project) {
private void sweepDeletedFileOrphans(ProjectInfo project, Set<String> ingestedNow) {
if (!autoInvalidateEnabled) {
return;
}
Path root = Path.of(project.root());
Set<String> tracked = astIngestService.distinctSourceFiles(project.name()).await().indefinitely();
List<String> gone = tracked.stream()
.filter(sf -> !Files.exists(root.resolve(sf)))
.filter(sf -> !Files.exists(root.resolve(sf)) && !ingestedNow.contains(sf))
.toList();
if (!gone.isEmpty()) {
LOG.infof("Refresh of '%s': removing nodes for %d deleted file(s): %s",
@@ -469,7 +567,7 @@ public class ProjectIngestService {
// must be reported, not accepted: it was silently listed as examined while
// nothing about it could ever be ingested, which is precisely the "I ingested
// 2 of your 3 files" invisibility this list exists to prevent.
|| SourceFiles.classify(root.resolve(sf).normalize()) == null)
|| SourceFiles.classify(root.resolve(sf).normalize(), project.language()) == null)
.toList();
List<String> ingestable = requested.stream().filter(sf -> !unresolved.contains(sf)).toList();
if (ingestable.isEmpty()) {
@@ -499,19 +597,28 @@ public class ProjectIngestService {
// call at the end — crash, container stop, an aborted deep refresh — the marker stays set and
// every later answer can say the graph is half-updated instead of looking clean.
markIngestStarted(project, coarse ? "tier1" : level.name().toLowerCase(Locale.ROOT));
progress.report(project.name(), "scanning the root");
Path root = Path.of(project.root());
List<String> excludeDirs = ingestExcludeDirs(project);
List<Candidate> allCandidates = walk(root, excludeDirs, project.language());
// NatJav: two files may claim one module name (a library homonym, or a copied header the
// migration never updated). Decided once over the whole walk, before anything is parsed —
// a single file cannot tell that its name is contested. Empty for every other language.
Map<String, NatJavIdentities.Rename> natJavRenames = NatJavIdentities.scan(root, allCandidates);
// NatJav: a view names only its DDM class; the table and column names live in that file.
NatJavDdmResolver ddms = NatJavDdmLibrary.scan(allCandidates);
CopycodeLibrary copycodes = CopycodeLibrary.scan(root, excludeDirs);
// Item 129: opt-in skip of files whose content is byte-identical to what the graph holds.
List<Candidate> candidates = changedOnly ? changedCandidates(project, root, excludeDirs, allCandidates)
: allCandidates;
Map<String, LocMetrics> userExit = UserExitMetrics.scan(root, project.userExitDir(), project.excludeDirs(), astIngestService);
Map<String, LocMetrics> userExit = UserExitMetrics.scan(root, project.userExitDir(), project.excludeDirs(), project.language(),
astIngestService);
// Item 192: the TypeScript context — Tier-1 needs only package.json; a deep pass runs the sidecar
// per workspace. A sidecar failure is reported as a failed pseudo-path and the files fall back to
// Tier-1, so the refresh completes and the response says what is missing.
List<IngestSummary.Failure> failed = new ArrayList<>();
TypeScriptProject ts = typescript.prepare(project, root, excludeDirs, coarse, failed);
candidates = withVirtualFiles(candidates, root, ts);
List<String> examinedFiles = candidates.stream()
.map(c -> relativeSourceFile(root, c.file()))
@@ -521,11 +628,12 @@ public class ProjectIngestService {
// mutable state; copycodes/userExit are read-only), so this scales the parse phase across cores.
// Results are collected in candidate order (futures list is parallel to candidates), so the
// downstream duplicate detection and persist order stay deterministic.
progress.report(project.name(), "parsing %d files".formatted(candidates.size()));
List<Parsed> parsed = new ArrayList<>();
try (ExecutorService parseExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<Parsed>> futures = candidates.stream()
.map(candidate -> parseExecutor.submit(
() -> parseCandidate(candidate, root, coarse, copycodes, ts, userExit, project)))
() -> parseCandidate(candidate, root, coarse, copycodes, ts, userExit, project, natJavRenames, ddms)))
.toList();
for (int i = 0; i < futures.size(); i++) {
try {
@@ -588,6 +696,8 @@ public class ProjectIngestService {
List<ParseResult> results = chunk.stream().map(Parsed::result).toList();
LOG.infof("Ingesting files %d-%d of %d into project '%s'",
from + 1, from + chunk.size(), toPersist.size(), project.name());
progress.report(project.name(), "persisting files %d-%d of %d".formatted(
from + 1, from + chunk.size(), toPersist.size()));
// Reconcile (delete stale nodes) only for a full parse — a coarse Tier-1 scan emits a
// subset of nodes and must not delete deep detail (item 58).
astIngestService.persistBatch(project.name(), results, !coarse).await().indefinitely();
@@ -767,7 +877,10 @@ public class ProjectIngestService {
int nodeLimit = Math.max(1, maxNodes != null ? maxNodes : defaultDeepNodes);
Path root = Path.of(project.root());
List<Candidate> candidates = walk(root, ingestExcludeDirs(project), project.language());
Map<NameKey, List<Candidate>> index = nameIndex(candidates);
Map<String, NatJavIdentities.Rename> natJavRenames = NatJavIdentities.scan(root, candidates);
NatJavDdmResolver ddms = NatJavDdmLibrary.scan(candidates);
Map<NameKey, List<Candidate>> index =
nameIndex(candidates, NatJavIdentities.graphNames(root, candidates, natJavRenames));
// Reverse index (Java): interface/base simple name -> files that implement/extend it, so a
// targeted ingest also pulls in the implementations/subclasses of any interface it reaches.
Map<String, List<Candidate>> implementorsByBase = buildImplementorIndex(root, candidates);
@@ -780,7 +893,7 @@ public class ProjectIngestService {
String label = seeds.size() == 1
? "'" + moduleNames.iterator().next() + "'"
: seeds.size() + " modules";
return bfsIngest(project, root, index, implementorsByBase, seeds, depthLimit, nodeLimit, label);
return bfsIngest(project, root, index, implementorsByBase, natJavRenames, ddms, seeds, depthLimit, nodeLimit, label);
}
/**
@@ -801,7 +914,10 @@ public class ProjectIngestService {
int nodeLimit = Math.max(1, maxNodes != null ? maxNodes : defaultDeepNodes);
Path root = Path.of(project.root());
List<Candidate> candidates = walk(root, ingestExcludeDirs(project), project.language());
Map<NameKey, List<Candidate>> index = nameIndex(candidates);
Map<String, NatJavIdentities.Rename> natJavRenames = NatJavIdentities.scan(root, candidates);
NatJavDdmResolver ddms = NatJavDdmLibrary.scan(candidates);
Map<NameKey, List<Candidate>> index =
nameIndex(candidates, NatJavIdentities.graphNames(root, candidates, natJavRenames));
Map<String, List<Candidate>> implementorsByBase = buildImplementorIndex(root, candidates);
Map<String, Candidate> byRelPath = new HashMap<>();
for (Candidate candidate : candidates) {
@@ -819,7 +935,7 @@ public class ProjectIngestService {
if (seeds.isEmpty()) {
return new IngestSummary(0, List.of(), List.of(), List.of(), List.of(), null);
}
return bfsIngest(project, root, index, implementorsByBase, seeds, defaultDeepDepth, nodeLimit,
return bfsIngest(project, root, index, implementorsByBase, natJavRenames, ddms, seeds, defaultDeepDepth, nodeLimit,
seeds.size() + " module(s)");
}
@@ -876,10 +992,13 @@ public class ProjectIngestService {
private IngestSummary bfsIngest(ProjectInfo project, Path root,
Map<NameKey, List<Candidate>> index,
Map<String, List<Candidate>> implementorsByBase,
Map<String, NatJavIdentities.Rename> natJavRenames,
NatJavDdmResolver ddms,
List<QueuedRef> seeds, int depthLimit, int nodeLimit, String label) {
long startedAt = System.nanoTime();
CopycodeLibrary copycodes = CopycodeLibrary.scan(root, ingestExcludeDirs(project));
Map<String, LocMetrics> userExit = UserExitMetrics.scan(root, project.userExitDir(), project.excludeDirs(), astIngestService);
Map<String, LocMetrics> userExit = UserExitMetrics.scan(root, project.userExitDir(), project.excludeDirs(), project.language(),
astIngestService);
List<IngestSummary.Failure> failed = new ArrayList<>();
// Item 192: a by-name deep ingest of a TypeScript module needs the sidecar facts too (3–5 s per
// workspace, run once for this call).
@@ -938,9 +1057,10 @@ public class ProjectIngestService {
String content = SourceFiles.read(candidate.file());
String sourceFile = relativeSourceFile(root, candidate.file());
examinedFiles.add(sourceFile);
ParseResult result = withUserExitMetrics(withShellMetrics(
astIngestService.parse(candidate.kind().language(), sourceFile, content, copycodes, ts),
content, candidate.kind().language()), project.generatedDir(), userExit);
ParseResult result = NatJavIdentities.apply(withUserExitMetrics(withShellMetrics(
astIngestService.parse(candidate.kind().language(), sourceFile, content, copycodes, ddms, ts),
content, candidate.kind().language()), project.generatedDir(), userExit),
sourceFile, natJavRenames);
LOG.infof("Ingesting %s into project '%s' [%s]", moduleNames(result), project.name(), sourceFile);
// Full deep parse of this module — reconcile so a by-name refresh purges its stale
// nodes (renamed/removed fields) rather than shadowing them (item 58).
@@ -1096,7 +1216,7 @@ public class ProjectIngestService {
return implementorsByBase;
}
private record Candidate(Path file, SourceFiles.Kind kind) {
record Candidate(Path file, SourceFiles.Kind kind) {
}
private record Parsed(Candidate candidate, ParseResult result) {

View File

@@ -0,0 +1,192 @@
package com.agenticcode.codeserver.service;
import com.agenticcode.neo4jstore.graph.IngestProgress;
import jakarta.annotation.PreDestroy;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;
import org.jspecify.annotations.Nullable;
import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Item 242: whole-project refreshes as jobs — one at a time per project, optionally detached from the
* HTTP request that started them.
*
* <p>A deep refresh of a large project can outlive its request: Quarkus drops an idle connection after
* 30 minutes, the client gets an empty {@code 200}, and the refresh finishes unseen on the server. A
* client that retries on that answer starts a second refresh over the running one. So every refresh
* runs as a job here, synchronous or not, and a second one for the same project is refused with the
* running job in the refusal ({@link RefreshRunningException}). An asynchronous job answers at once
* with its id; {@link #get} reports its state, the current phase from {@link IngestProgress}, and the
* {@link IngestSummary} once it ends.
*
* <p>Jobs live in memory only; the last {@value #KEEP} are kept. A server restart forgets them — the
* run itself is then cut off too, and the project's {@code ingest.incomplete} flag (item 129) is what
* says so.
*/
@ApplicationScoped
public class RefreshJobs {
private static final Logger LOG = Logger.getLogger(RefreshJobs.class);
static final int KEEP = 20;
public enum Status { RUNNING, DONE, FAILED }
/**
* One job as the API reports it. {@code progress} is the running job's current phase, {@code null}
* once it ended; {@code result} is set when it is {@code DONE}, {@code error} when it {@code FAILED}.
*/
public record RefreshJob(String id, String project, String kind, Status status, String startedAt,
@Nullable String finishedAt, long elapsedSeconds, @Nullable String progress,
@Nullable IngestSummary result, @Nullable String error) {
}
/**
* Thrown instead of starting a second refresh of a project that is already being refreshed.
*/
public static final class RefreshRunningException extends RuntimeException {
private final RefreshJob running;
RefreshRunningException(RefreshJob running) {
super("A " + running.kind() + " of project '" + running.project() + "' is already running (job "
+ running.id() + ")");
this.running = running;
}
public RefreshJob running() {
return running;
}
}
@FunctionalInterface
public interface Work {
IngestSummary run() throws IOException;
}
private final class Entry {
final String id = UUID.randomUUID().toString();
final String project;
final String kind;
final Instant startedAt = Instant.now();
volatile Status status = Status.RUNNING;
volatile @Nullable Instant finishedAt;
volatile @Nullable IngestSummary result;
volatile @Nullable String error;
Entry(String project, String kind) {
this.project = project;
this.kind = kind;
}
RefreshJob snapshot() {
Instant end = finishedAt;
Instant until = end != null ? end : Instant.now();
return new RefreshJob(id, project, kind, status, startedAt.truncatedTo(ChronoUnit.SECONDS).toString(),
end == null ? null : end.truncatedTo(ChronoUnit.SECONDS).toString(),
until.getEpochSecond() - startedAt.getEpochSecond(),
status == Status.RUNNING ? progress.current(project) : null, result, error);
}
}
private final IngestProgress progress;
private final ConcurrentHashMap<String, Entry> runningByProject = new ConcurrentHashMap<>();
// newest first; guarded by itself
private final Deque<Entry> recent = new ArrayDeque<>();
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
public RefreshJobs(IngestProgress progress) {
this.progress = progress;
}
/**
* Runs {@code work} on the calling thread as a job of {@code project}.
*
* @throws RefreshRunningException when a refresh of the project is already running
*/
public IngestSummary runNow(String project, String kind, Work work) throws IOException {
Entry entry = claim(project, kind);
try {
IngestSummary summary = work.run();
finish(entry, summary, null);
return summary;
} catch (IOException | RuntimeException | Error e) {
finish(entry, null, e.toString());
throw e;
}
}
/**
* Starts {@code work} on its own thread and returns the running job at once.
*
* @throws RefreshRunningException when a refresh of the project is already running
*/
public RefreshJob start(String project, String kind, Work work) {
Entry entry = claim(project, kind);
executor.submit(() -> {
try {
finish(entry, work.run(), null);
} catch (Exception | Error e) {
LOG.errorf(e, "Refresh job %s (%s of project '%s') failed", entry.id, kind, project);
finish(entry, null, e.toString());
}
});
return entry.snapshot();
}
public @Nullable RefreshJob get(String project, String id) {
synchronized (recent) {
return recent.stream().filter(e -> e.id.equals(id) && e.project.equals(project))
.findFirst().map(Entry::snapshot).orElse(null);
}
}
/**
* The kept jobs of {@code project}, newest first.
*/
public List<RefreshJob> list(String project) {
synchronized (recent) {
return recent.stream().filter(e -> e.project.equals(project)).map(Entry::snapshot).toList();
}
}
private Entry claim(String project, String kind) {
Entry entry = new Entry(project, kind);
Entry running = runningByProject.putIfAbsent(project, entry);
if (running != null) {
throw new RefreshRunningException(running.snapshot());
}
progress.clear(project);
synchronized (recent) {
recent.addFirst(entry);
// never evict a running job: its id is what the caller polls
Entry oldest;
while (recent.size() > KEEP && (oldest = recent.peekLast()) != null && oldest.status != Status.RUNNING) {
recent.removeLast();
}
}
return entry;
}
private void finish(Entry entry, @Nullable IngestSummary summary, @Nullable String error) {
entry.result = summary;
entry.error = error;
entry.finishedAt = Instant.now();
entry.status = error == null ? Status.DONE : Status.FAILED;
progress.clear(entry.project);
runningByProject.remove(entry.project, entry);
}
@PreDestroy
void shutdown() {
executor.shutdownNow();
}
}

View File

@@ -18,6 +18,11 @@ import java.util.Locale;
*
* <p>Java classes carry {@link NodeType#MODULE}; Natural {@code .nat}/{@code .nsn} programs are
* {@code MODULE}s and {@code .lda}/{@code .pda}/{@code .gda} data areas (local/parameter/global) are
* {@link NodeType#DATA_STRUCTURE}s. Files with any other extension are not ingestible and classify
* to {@code null}.
*
* <p>NatJav (Natural transpiled to Java) breaks the extension-only rule: its files are {@code .java},
* so the <em>project's</em> declared language decides — see {@link #classify(Path, String)}.
* {@link NodeType#DATA_STRUCTURE}s; TypeScript {@code .ts}/{@code .tsx} files (not {@code .d.ts}) and
* plain {@code .css} files are {@code MODULE}s (item 192). Files with any other extension are not
* ingestible and classify to {@code null}.
@@ -34,14 +39,57 @@ final class SourceFiles {
}
/**
* @return the classification of {@code file} by extension, or {@code null} if it is not an
* ingestible source file (e.g. {@code .meta}).
* The {@code language} of a project whose {@code .java} files are transpiled Natural rather than
* hand-written Java, and must therefore be read by the NatJav parser.
*/
static final String NATJAV_PROJECT_LANGUAGE = "natjav";
/**
* Suffix the maxenso transpiler appends to a Natural data area's class name. Every one of the
* 5,818 {@code NaturalData} classes in the reference corpus carries it and no other class does,
* so it classifies a file the way {@code .lda}/{@code .pda}/{@code .gda} does for Natural. The
* <em>name</em> of the resulting node still comes from the parser, which reads the class's
* {@code @Natural(moduleName=...)} — the suffix is not stripped by string surgery here.
*/
private static final String NATJAV_DATA_AREA_SUFFIX = "_da.java";
/**
* @return {@link #classify(Path, String)} with no project in scope — the extension alone, so a
* {@code .java} file is {@code JAVA}.
*/
static @Nullable Kind classify(Path file) {
return classify(file, null);
}
/**
* @param file the file to classify
* @param projectLanguage the owning project's declared {@code language}, or {@code null} if the
* caller has no project in scope. Only {@code "natjav"} changes the
* outcome: a NatJav file is a {@code .java} file, so the extension alone
* cannot distinguish it from hand-written Java and the project must say.
* @return the classification of {@code file}, or {@code null} if it is not an ingestible source
* file (e.g. {@code .meta})
*/
static @Nullable Kind classify(Path file, @Nullable String projectLanguage) {
String name = file.getFileName().toString().toLowerCase(Locale.ROOT);
if (name.endsWith(".java")) {
if (NATJAV_PROJECT_LANGUAGE.equalsIgnoreCase(projectLanguage)) {
return new Kind(Language.NATJAV,
name.endsWith(NATJAV_DATA_AREA_SUFFIX) ? NodeType.DATA_STRUCTURE : NodeType.MODULE);
}
return new Kind(Language.JAVA, NodeType.MODULE);
}
// Item 215: a NatJav copycode (`YVABNOUT_member.jcopy`) is a module of its own — the file the
// include markers in 6,124 programs name. Only a natjav project reads it.
if (name.endsWith(".jcopy") && NATJAV_PROJECT_LANGUAGE.equalsIgnoreCase(projectLanguage)) {
return new Kind(Language.NATJAV, NodeType.MODULE);
}
// Items 225/226: configuration and SOAP descriptors. Only these names: an arbitrary .xml (a Maven
// pom, test data, a generated schema) is not configuration.
if ((isConfig(name) || isResourceXml(file, name) || isJaxwsBinding(file, name) || isOpenApiSpec(file, name))
&& !isExcludedConfig(file, name)) {
return new Kind(Language.CONFIG, NodeType.MODULE);
}
if (name.endsWith(".nat") || name.endsWith(".nsn")) {
return new Kind(Language.NATURAL, NodeType.MODULE);
}
@@ -54,6 +102,10 @@ final class SourceFiles {
if (name.endsWith(".css")) {
return new Kind(Language.CSS, NodeType.MODULE);
}
// Item 229: an Angular project's component templates and SCSS stylesheets (same parser as CSS).
if ("angular".equalsIgnoreCase(projectLanguage) && (name.endsWith(".html") || name.endsWith(".scss"))) {
return new Kind(Language.CSS, NodeType.MODULE);
}
return null;
}
@@ -64,11 +116,134 @@ final class SourceFiles {
*/
static boolean ingestedBy(Language language, @Nullable String projectLanguage) {
return switch (language) {
case JAVA, NATURAL -> true;
case TYPESCRIPT, CSS -> "typescript".equalsIgnoreCase(projectLanguage);
case JAVA, NATURAL, NATJAV -> true;
case TYPESCRIPT, CSS -> "typescript".equalsIgnoreCase(projectLanguage) || "angular".equalsIgnoreCase(projectLanguage);
// Items 225/226: the configuration of a Java application (a NatJav one is Java too).
case CONFIG -> "java".equalsIgnoreCase(projectLanguage) || NATJAV_PROJECT_LANGUAGE.equalsIgnoreCase(projectLanguage);
};
}
/**
* Items 225/226: {@code .properties}, WSDL, Axis2 {@code services.xml}, and the XML descriptors that
* wire a Java application ({@code persistence.xml}, {@code sun-jaxws.xml}, {@code web.xml}, Spring
* contexts). {@code name} is the lower-cased file name.
*/
/**
* Item 226: an XML file where an application keeps its wiring — under {@code src/main/resources},
* {@code WEB-INF} or {@code META-INF}, and whose root element is a configuration format. Spring
* contexts are named freely ({@code kosDataAccessDBConfiguration.xml}), so a name list missed K3's
* datasource; but the same directories hold data ({@code VERSICHERER_521.xml}, {@code DomainValues},
* JAXB {@code bindings}), and taking every XML there ran the server out of heap on K3 — hence the
* root-element check. {@code pom.xml} is build configuration.
*/
static boolean isResourceXml(Path file, String name) {
if (!name.endsWith(".xml") || name.equals("pom.xml")) {
return false;
}
String path = file.toString().replace('\\', '/');
if (!(path.contains("/src/main/resources/") || path.contains("/WEB-INF/") || path.contains("/META-INF/"))) {
return false;
}
String root = rootElement(file);
return root != null && CONFIG_XML_ROOTS.contains(root);
}
/**
* Item 239: a JAX-WS {@code wsimport} binding ({@code WS_X-binding.xml}, root {@code bindings} in the
* JAX-WS namespace, with a {@code wsdlLocation}). JAXB bindings share the root element name — the
* namespace and the attribute keep them out, as {@link #isResourceXml} does for all other bindings.
*/
static boolean isJaxwsBinding(Path file, String name) {
if (!name.endsWith(".xml") || !name.contains("binding")) {
return false;
}
try (java.io.InputStream in = Files.newInputStream(file)) {
String head = new String(in.readNBytes(4096), StandardCharsets.ISO_8859_1);
return "bindings".equals(rootElement(file)) && head.contains("wsdlLocation")
&& (head.contains("java.sun.com/xml/ns/jaxws") || head.contains("jakarta.ee/xml/ns/jaxws"));
} catch (IOException e) {
return false;
}
}
/**
* Item 253: an OpenAPI/Swagger document under {@code src/main/resources} — the routing of a spec-first
* Spring server, whose controllers are generated from it at build time. Other YAML/JSON there is data
* or configuration of another kind, hence the check of the document's first lines.
*/
static boolean isOpenApiSpec(Path file, String name) {
if (!(name.endsWith(".yaml") || name.endsWith(".yml") || name.endsWith(".json"))
|| !file.toString().replace('\\', '/').contains("/src/main/resources/")) {
return false;
}
try (java.io.InputStream in = Files.newInputStream(file)) {
return com.agenticcode.parserjava.OpenApiSpecParser.looksLikeSpec(
new String(in.readNBytes(4096), StandardCharsets.UTF_8));
} catch (IOException e) {
return false;
}
}
/**
* Root elements of the XML configuration formats (Spring, log4j2, Tomcat/JBoss, Servlet, JSF, JPA,
* JAX-WS, Axis2, Dozer, Struts/Tiles, Bean Validation, XML properties).
*/
private static final java.util.Set<String> CONFIG_XML_ROOTS = java.util.Set.of(
"beans", "Configuration", "configuration", "Context", "web-app", "web-fragment", "faces-config",
"persistence", "entity-mappings", "hibernate-configuration", "endpoints", "serviceGroup", "service",
"mappings", "config", "validation-config", "struts", "tiles-definitions", "jboss-web",
"jboss-deployment-structure", "properties");
private static final java.util.regex.Pattern XML_NOISE = java.util.regex.Pattern.compile(
"<\\?.*?\\?>|<!--.*?-->|<!DOCTYPE[^>]*>", java.util.regex.Pattern.DOTALL);
private static final java.util.regex.Pattern XML_ELEMENT = java.util.regex.Pattern.compile("<([A-Za-z_][\\w:.-]*)");
/**
* @return the local name of the file's root element, read from its first 8 KB, or {@code null}
*/
static @Nullable String rootElement(Path file) {
try (java.io.InputStream in = Files.newInputStream(file)) {
String head = new String(in.readNBytes(8192), StandardCharsets.ISO_8859_1);
java.util.regex.Matcher m = XML_ELEMENT.matcher(XML_NOISE.matcher(head).replaceAll(""));
if (!m.find()) {
return null;
}
String element = m.group(1);
return element.substring(element.indexOf(':') + 1);
} catch (IOException e) {
return null;
}
}
/**
* Item 226: configuration that is not worth a node per line — test resources, locale bundles
* ({@code Translations_de.properties}: UI text, 3 MB on K3) and anything over 1 MB.
*/
static boolean isExcludedConfig(Path file, String name) {
String path = file.toString().replace('\\', '/');
if (path.contains("/src/test/")) {
return true;
}
if (name.endsWith(".properties") && LOCALE_BUNDLE.matcher(file.getFileName().toString()).find()) {
return true;
}
try {
return Files.size(file) > 1_048_576L;
} catch (IOException e) {
return true;
}
}
private static final java.util.regex.Pattern LOCALE_BUNDLE =
java.util.regex.Pattern.compile("_[a-z]{2}(_[A-Z]{2})?\\.properties$");
static boolean isConfig(String name) {
return name.endsWith(".properties") || name.endsWith(".wsdl")
|| name.equals("services.xml") || name.equals("persistence.xml") || name.equals("sun-jaxws.xml")
|| name.equals("web.xml") || (name.startsWith("applicationcontext") && name.endsWith(".xml"))
|| name.endsWith("-context.xml");
}
/**
* @return the uppercased filename stem (name without extension), used as the case-insensitive
* lookup key for resolving {@code CALLNAT}/{@code PERFORM}/{@code USING}/{@code extends} targets.
@@ -93,16 +268,24 @@ final class SourceFiles {
}
/**
* @return whether any component of {@code file}'s path relative to {@code root} matches one of
* {@code excludeDirs} (case-insensitive)
* @return whether {@code file}'s path relative to {@code root} is excluded by one of {@code excludeDirs}
* (case-insensitive): a bare name ({@code target}) matches any path component; an entry starting with
* {@code /} ({@code /Koala}) is anchored at the root and matches only that directory — so excluding the
* {@code Koala} front end at the top no longer drops every {@code at/k3/koala} Java package too.
*/
static boolean isExcluded(Path root, Path file, List<String> excludeDirs) {
if (excludeDirs.isEmpty()) {
return false;
}
for (Path part : root.relativize(file)) {
Path relative = root.relativize(file);
for (String entry : excludeDirs) {
if (entry.startsWith("/") && isUnderAnchored(relative, entry)) {
return true;
}
}
for (Path part : relative) {
String name = part.toString();
if (excludeDirs.stream().anyMatch(d -> d.equalsIgnoreCase(name))) {
if (excludeDirs.stream().anyMatch(d -> !d.startsWith("/") && d.equalsIgnoreCase(name))) {
return true;
}
}
@@ -110,9 +293,23 @@ final class SourceFiles {
}
/**
* The parser a file goes to. {@code CSS} shares the TypeScript parser but has its own line counter.
* @return whether {@code relative} lies in (or is) the root-anchored directory {@code entry} ({@code /a/b})
*/
enum Language {JAVA, NATURAL, TYPESCRIPT, CSS}
static boolean isUnderAnchored(Path relative, String entry) {
String dir = entry.replaceAll("^/+|/+$", "");
String path = relative.toString().replace('\\', '/');
return !dir.isEmpty() && (path.equalsIgnoreCase(dir)
|| path.length() > dir.length() && path.charAt(dir.length()) == '/'
&& path.substring(0, dir.length()).equalsIgnoreCase(dir));
}
/**
* The parser a source file is read by. {@code NATJAV} is Natural transpiled to Java: its files
* are syntactically Java but map onto the Natural half of the unified AST, which is why it is a
* language of its own rather than a flavour of {@code JAVA}. {@code CSS} shares the TypeScript
* parser but has its own line counter.
*/
enum Language {JAVA, NATURAL, NATJAV, TYPESCRIPT, CSS, CONFIG}
/**
* Classification of an ingestible source file.

View File

@@ -82,12 +82,15 @@ public class TypeScriptSidecarService {
*/
public TypeScriptProject prepare(ProjectInfo project, Path root, List<String> excludeDirs, boolean coarse,
List<IngestSummary.Failure> failures) {
if (!"typescript".equalsIgnoreCase(project.language() == null ? "" : project.language())) {
String language = project.language() == null ? "" : project.language();
// Item 229: an `angular` project runs the same sidecar with the Angular facts switched on.
boolean angular = "angular".equalsIgnoreCase(language);
if (!"typescript".equalsIgnoreCase(language) && !angular) {
return TypeScriptProject.NONE;
}
TypeScriptProject context;
try {
context = TypeScriptProject.scan(root);
context = angular ? TypeScriptProject.scanAngular(root) : TypeScriptProject.scan(root);
} catch (IOException e) {
LOG.warnf("Could not read package.json under '%s' (%s); using built-in package list", root, e.toString());
context = TypeScriptProject.NONE;
@@ -103,14 +106,15 @@ public class TypeScriptSidecarService {
return context;
}
List<String> workspaces = context.workspaces().isEmpty() ? List.of(".") : context.workspaces().stream()
.filter(ws -> excludeDirs.stream().noneMatch(x -> x.equalsIgnoreCase(ws) || x.equalsIgnoreCase(Path.of(ws).getFileName().toString())))
.filter(ws -> excludeDirs.stream().noneMatch(x -> x.startsWith("/") ? SourceFiles.isUnderAnchored(Path.of(ws), x)
: x.equalsIgnoreCase(ws) || x.equalsIgnoreCase(Path.of(ws).getFileName().toString())))
.sorted()
.toList();
List<TypeScriptFacts> parts = new ArrayList<>();
for (String workspace : workspaces) {
long started = System.nanoTime();
try {
TypeScriptFacts facts = sidecar.extract(root, workspace, null);
TypeScriptFacts facts = sidecar.extract(root, workspace, null, angular ? "angular" : null);
parts.add(facts);
LOG.infof("TypeScript sidecar: workspace '%s' of '%s' -> %d files in %d ms", workspace, project.name(),
facts.byFile().size(), (System.nanoTime() - started) / 1_000_000);

View File

@@ -33,12 +33,16 @@ final class UserExitMetrics {
/**
* @return module stem (upper-case) → the user-exit file's LoC/SLoC, for every ingestible file
* under a {@code userExitDir} path component. Empty when {@code userExitDir} is {@code null} or the
* under a {@code userExitDir} path component. {@code projectLanguage} is what tells a NatJav
* project's {@code .java} files apart from hand-written Java, so the metrics are counted by the
* right {@code LineCounter}. Empty when {@code userExitDir} is {@code null} or the
* root cannot be walked. When two user-exit trees hold the same stem, the first in sorted walk
* order wins (deterministic; narrow the scope via {@code excludeDirs} if undesired).
*/
static Map<String, LocMetrics> scan(Path root, @org.jspecify.annotations.Nullable String userExitDir,
List<String> excludeDirs, AstIngestService astIngestService) {
List<String> excludeDirs,
@org.jspecify.annotations.Nullable String projectLanguage,
AstIngestService astIngestService) {
Map<String, LocMetrics> byStem = new HashMap<>();
if (userExitDir == null || userExitDir.isBlank()) {
return byStem;
@@ -53,7 +57,7 @@ final class UserExitMetrics {
if (!hasComponent(root, file, userExitDir) || SourceFiles.isExcluded(root, file, otherExcludes)) {
continue;
}
SourceFiles.Kind kind = SourceFiles.classify(file);
SourceFiles.Kind kind = SourceFiles.classify(file, projectLanguage);
if (kind == null) {
continue;
}

View File

@@ -3,7 +3,7 @@ quarkus.http.port=8787
# AgenticCode's own release counter (not the Maven project version) — bump this by hand for each
# release. Single source of truth for the startup log line, GET /api/version, and the OpenAPI
# info version (referenced below via property expression, not duplicated).
agenticcode.version=334
agenticcode.version=401
# OpenAPI / Swagger UI (item 48) — the generated spec is the contract the web-UI TS client
# is generated against. Served at /q/openapi (yaml/json); Swagger UI at /q/swagger-ui in dev.
mp.openapi.extensions.smallrye.info.title=AgenticCode API

View File

@@ -524,6 +524,40 @@ class AnalysisResourceIT {
.body("find { it.name == 'com.example.sample.SampleLegacyEntity' }.annotations", equalTo("Entity"));
}
/**
* Item 210: {@code exact=true} compares the whole name, so a prefix no longer matches.
*/
@Test
void searchAnnotationExactMatchesTheWholeNameOnly() {
given()
.queryParam("name", "Colum").queryParam("exact", true)
.when().get("/api/projects/" + PROJECT + "/search/annotation")
.then().statusCode(200)
.body("$", empty());
given()
.queryParam("name", "Column").queryParam("exact", true).queryParam("type", "FIELD")
.when().get("/api/projects/" + PROJECT + "/search/annotation")
.then().statusCode(200)
.body("name", hasItem("vid"));
}
/**
* Item 209: {@code unresolved=true} lists the placeholder modules — and only those.
*/
@Test
void modulesUnresolvedListsOnlyPlaceholders() {
given()
.queryParam("unresolved", true)
.when().get("/api/projects/" + PROJECT + "/modules")
.then().statusCode(200)
.body("$", not(empty()))
.body("sourceFile", everyItem(equalTo("")));
given()
.when().get("/api/projects/" + PROJECT + "/modules")
.then().statusCode(200)
.body("sourceFile", everyItem(not(equalTo(""))));
}
@Test
void inspectNodeReturnsEveryProperty() {
String id = sampleLegacyEntityNodeId();

View File

@@ -0,0 +1,379 @@
package com.agenticcode.codeserver.api;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
/**
* Item 234 (gap L5): an Angular front end ({@code fixtures/angular-api}, no node_modules, like Koala) and the
* Java back end it calls. A hand-written HttpClient service is an outbound endpoint per method whose URL keeps
* the context root ({@code /ShopData/api/v1/orders/{id}}); it pairs with the Spring handler whose path is a
* unique suffix of it. The OpenAPI client the workspace generates at build time is not on disk: its modules
* are built from the spec, and its operations pair with the delegate controller's methods by operationId.
* Item 232: the NGXS cart state of the same fixture on the item-194 store endpoints. Item 233: its nested
* SCSS rules with their design tokens, and the inline styles of its template.
*/
@QuarkusTest
class AngularApiIT {
private static final String BACKEND = "item234-backend";
private static final String FRONTEND = "item234-frontend";
@TempDir
static Path backendRoot;
@TempDir
static Path frontendRoot;
@BeforeAll
static void ingest() throws IOException {
assumeTrue(CounterpartsIT.sidecarAvailable(), "node + sidecar/node_modules/typescript not installed here");
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
Path fixture = Path.of("../ac-parser-typescript/src/test/resources/fixtures/angular-api").toAbsolutePath().normalize();
try (Stream<Path> files = Files.walk(fixture)) {
files.filter(Files::isRegularFile).forEach(f -> {
Path relative = fixture.relativize(f);
Path dir = relative.getParent() == null ? frontendRoot : frontendRoot.resolve(relative.getParent().toString());
write(dir, relative.getFileName().toString(), read(f));
});
}
write(backendRoot.resolve("ShopData/src/main/java/shop/data"), "OrderController.java", """
package shop.data;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/v1/orders")
public class OrderController {
@GetMapping("/{id}")
public String load(String id) {
return id;
}
@PostMapping
public String store(String order) {
return order;
}
}
""");
// two handlers with the same path in the same application: a suffix match is ambiguous and must not link
for (String cls : List.of("TwiceController", "AgainController")) {
write(backendRoot.resolve("ShopData/src/main/java/shop/data"), cls + ".java", """
package shop.data;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class %s {
@GetMapping("/v1/twice")
public String twice() {
return "";
}
}
""".formatted(cls));
}
write(backendRoot.resolve("ShopData/src/main/java/shop/data"), "InfoController.java", """
package shop.data;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class InfoController {
@GetMapping("/v1/info/version")
public String version() {
return "1";
}
}
""");
// `/ShopApi/api/login` ends in `/login`, but ShopData is another application: no pairing
write(backendRoot.resolve("ShopData/src/main/java/shop/data"), "LoginController.java", """
package shop.data;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
@PostMapping("/login")
public boolean login() {
return true;
}
}
""");
write(backendRoot.resolve("ShopApi/src/main/java/shop/api"), "OrdersController.java", """
package shop.api;
import shop.api.generated.OrdersApiDelegate;
public class OrdersController implements OrdersApiDelegate {
public String listOrders() {
return "";
}
public String deleteOrder(String id) {
return id;
}
public String archiveOrders() {
return "";
}
}
""");
// item 253: a spec-first server — the routing is in swagger.yaml, the class only implements the
// generated delegate. `purgeOrders` has no implementation (the generated default answers 501).
write(backendRoot.resolve("ShopApi/src/main/resources"), "swagger.yaml", """
openapi: "3.0.3"
info:
title: ShopApi
version: "1"
servers:
- url: /ShopApi
paths:
/api/login:
post:
operationId: login
tags:
- Authentication
responses:
200:
description: ok
/api/login/spnego:
get:
operationId: spnego
tags:
- Authentication
parameters:
- name: forcedTenant
in: query
schema:
type: string
responses:
200:
description: ok
/api/v1/orders:
get:
operationId: listOrders
tags:
- Orders
responses:
200:
description: ok
/api/v1/orders/purge:
post:
operationId: purgeOrders
tags:
- Orders
responses:
200:
description: ok
""");
write(backendRoot.resolve("ShopApi/src/main/java/shop/api"), "AuthenticationController.java", """
package shop.api;
import shop.api.generated.AuthenticationApiDelegate;
public class AuthenticationController implements AuthenticationApiDelegate {
public boolean login() {
return true;
}
public boolean spnego(String forcedTenant) {
return true;
}
}
""");
// item 252: tag `i18n`, delegate `I18nApiDelegate`
write(backendRoot.resolve("ShopApi/src/main/java/shop/api"), "TranslationController.java", """
package shop.api;
import shop.api.generated.I18nApiDelegate;
public class TranslationController implements I18nApiDelegate {
public String getTranslations() {
return "";
}
}
""");
write(backendRoot.resolve("ShopApi/src/main/java/shop/api"), "OrderDTO.java", """
package shop.api;
public class OrderDTO {
private String id;
private Double total;
}
""");
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, backendRoot.toString(), null, "java", null, null))
.when().post("/api/projects/" + BACKEND).then().statusCode(201);
given().when().post("/api/projects/" + BACKEND + "/refresh?deep=true").then().statusCode(200);
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, frontendRoot.toString(), null, "angular", null, null,
List.of(BACKEND)))
.when().post("/api/projects/" + FRONTEND).then().statusCode(201);
given().when().post("/api/projects/" + FRONTEND + "/refresh?deep=true").then().statusCode(200)
.body("failed", empty());
}
private static String read(Path file) {
try {
return Files.readString(file);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static void write(Path dir, String fileName, String content) {
try {
Files.createDirectories(dir);
Files.writeString(dir.resolve(fileName), content);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static io.restassured.response.Response get(String path) {
return given().when().get("/api/projects/" + FRONTEND + path);
}
@Test
void httpClientMethodsAndOpenApiOperationsAreOutboundEndpoints() {
get("/rest-endpoints").then().statusCode(200)
.body("outbound", everyItem(equalTo(true)))
.body("find { it.handler == 'OrderApiService.load' }.path", equalTo("/ShopData/api/v1/orders/{id}"))
.body("find { it.handler == 'OrderApiService.store' }.httpMethod", equalTo("POST"))
.body("find { it.handler == 'OrdersService.listOrders' }.path", equalTo("/ShopApi/api/v1/orders"))
.body("find { it.handler == 'OrdersService.listOrders' }.module", equalTo("app/src/generated/api/orders.service"))
.body("find { it.handler == 'OrdersService.deleteOrder' }.httpMethod", equalTo("DELETE"));
}
/**
* Item 253: the operations of a spec-first server's swagger.yaml are endpoints of the methods that
* implement them; an operation without one is marked, not listed.
*/
@Test
void theSpecOfASpecFirstServerMakesItsDelegateMethodsEndpoints() {
given().when().get("/api/projects/" + BACKEND + "/rest-endpoints").then().statusCode(200)
.body("find { it.handler == 'login' }.path", equalTo("/ShopApi/api/login"))
.body("find { it.handler == 'login' }.httpMethod", equalTo("POST"))
.body("find { it.handler == 'login' }.module", equalTo("shop.api.AuthenticationController"))
.body("find { it.handler == 'listOrders' }.path", equalTo("/ShopApi/api/v1/orders"))
.body("handler", not(hasItem("purgeOrders")));
String purge = given().queryParam("name", "purgeOrders").queryParam("type", "FUNCTION")
.when().get("/api/projects/" + BACKEND + "/search/identifier")
.then().statusCode(200).body("size()", equalTo(1)).extract().path("[0].id");
given().when().get("/api/projects/" + BACKEND + "/nodes/" + purge).then().statusCode(200)
.body("implemented", equalTo("false"))
.body("specPath", equalTo("/ShopApi/api/v1/orders/purge"));
String list = given().queryParam("name", "listOrders").queryParam("type", "FUNCTION")
.when().get("/api/projects/" + BACKEND + "/search/identifier")
.then().statusCode(200).extract().path("find { it.sourceFile.endsWith('swagger.yaml') }.id");
given().when().get("/api/projects/" + BACKEND + "/nodes/" + list).then().statusCode(200)
.body("implementedBy", equalTo("shop.api.OrdersController.listOrders"));
}
@Test
void theGeneratedClientIsAModuleWithoutAFileOnDisk() {
get("/modules/orders.service/context").then().statusCode(200)
.body("functions.name", hasItems("OrdersService", "OrdersService.listOrders", "OrdersService.deleteOrder"));
// survives the deleted-file sweep of a second refresh
given().when().post("/api/projects/" + FRONTEND + "/refresh").then().statusCode(200);
get("/modules/orderDTO/context").then().statusCode(200);
}
@Test
void componentCallsLandOnTheEndpoints() {
get("/modules/order-page.component/callees").then().statusCode(200)
.body("items.name", hasItems("app/src/app/order-api.service", "app/src/generated/api/orders.service"));
get("/modules/order-api.service/callers").then().statusCode(200)
.body("items.name", hasItem("app/src/app/order-page.component"));
}
@Test
void endpointsPairWithTheirHandlers() {
get("/counterparts?kind=rest").then().statusCode(200)
// context root and servlet path are not part of the handler's path: unique suffix match
.body("find { it.name == 'OrderApiService.load' }.counterpartName", equalTo("load"))
.body("find { it.name == 'OrderApiService.load' }.counterpartModule", equalTo("shop.data.OrderController"))
.body("find { it.name == 'OrderApiService.store' }.counterpartName", equalTo("store"))
.body("find { it.name == 'OrderApiService.twice' }.counterpartName", nullValue())
// not K3-style ShopData's `/login` (another application), but the spec-first ShopApi's
// `POST /ShopApi/api/login`, served by the delegate method (item 253)
.body("find { it.name == 'OrderApiService.login' }.counterpartModule", equalTo("shop.api.AuthenticationController"))
.body("find { it.name == 'OrderApiService.login' }.counterpartName", equalTo("login"))
// `'/ShopApi/api/login/spnego' + param`, param an optional `?forcedTenant=…` (item 259)
.body("find { it.name == 'OrderApiService.spnego' }.counterpartName", equalTo("spnego"))
// `this.versionUrl`, a private property initialised from environment.apiUrl (item 260)
.body("find { it.name == 'OrderApiService.version' }.counterpartModule", equalTo("shop.data.InfoController"))
// generated delegate: <Tag>ApiDelegate + operationId
.body("find { it.name == 'OrdersService.listOrders' }.counterpartName", equalTo("listOrders"))
.body("find { it.name == 'OrdersService.listOrders' }.counterpartModule", equalTo("shop.api.OrdersController"))
.body("find { it.name == 'OrdersService.deleteOrder' }.counterpartName", equalTo("deleteOrder"))
// operationId `ArchiveOrders`: the generated method on both sides is `archiveOrders`
.body("find { it.name == 'OrdersService.archiveOrders' }.counterpartName", equalTo("archiveOrders"))
// tag `i18n`: the generator's class is `I18nApiDelegate`, the comparison is on that form (item 252)
.body("find { it.name == 'I18nService.getTranslations' }.counterpartModule", equalTo("shop.api.TranslationController"))
.body("find { it.name == 'OrdersService.deleteOrder' }.counterpartProject", equalTo(BACKEND));
get("/counterparts?kind=dto").then().statusCode(200)
.body("find { it.name == 'OrderDTO' }.counterpartModule", equalTo("shop.api.OrderDTO"));
}
@Test
void anNgxsStateIsASliceWithItsActionsAsReducers() {
get("/store?slice=cart").then().statusCode(200)
.body("[0].module", equalTo("app/src/app/cart/cart.state"))
.body("[0].stateType", equalTo("CartModel"))
.body("[0].fields.name", contains("items", "total", "form"))
.body("[0].reducers", equalTo(2));
get("/store/cart/accesses?mode=writes").then().statusCode(200)
.body("findAll { it.function == '[Cart] Clear' }.field", hasItems("items", "total"))
.body("find { it.function == '[Cart] Add item' }.field", equalTo("items"))
.body("find { it.via == 'ngxsForm' }.function", equalTo("CartComponent"))
.body("find { it.via == 'ngxsForm' }.sourceFile", equalTo("app/src/app/cart/cart.component.html"));
get("/store/cart/accesses?mode=reads&module=app/src/app/cart/cart.component").then().statusCode(200)
.body("via", hasItems("select", "selectSnapshot", "ngxsForm"))
.body("find { it.via == 'selectSnapshot' }.field", equalTo("total"));
}
@Test
void dispatchCallsTheReducerAndTheReducerCallsTheService() {
get("/modules/cart.component/callees").then().statusCode(200)
.body("items.name", hasItem("app/src/app/cart/cart.state"));
get("/modules/cart.state/functions").then().statusCode(200)
.body("find { it.name == '[Cart] Add item' }", notNullValue());
get("/modules/order-api.service/callers").then().statusCode(200)
.body("items.name", hasItem("app/src/app/cart/cart.state"));
}
@Test
void scssRulesAreNestedAndTemplateStylesAreSitedInTheTemplate() {
get("/styles?module=cart.component.scss").then().statusCode(200)
.body("name", contains(".cart@4", ".cart .line@6", ".cart:hover@9"))
.body("find { it.name == '.cart@4' }.tokens", contains("colors.primary-palette.50"))
.body("find { it.name == '.cart .line@6' }.tokens", contains("colors.grey"))
.body("find { it.name == '.cart:hover@9' }.tokens", contains("--cart-hover"));
get("/styles?kind=template").then().statusCode(200)
.body("[0].function", equalTo("CartComponent"))
.body("[0].element", equalTo("div"))
.body("[0].sourceFile", equalTo("app/src/app/cart/cart.component.html"))
.body("[0].lineNo", equalTo(3))
.body("[0].literals", equalTo("#333,10px"));
}
}

View File

@@ -0,0 +1,142 @@
package com.agenticcode.codeserver.api;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
/**
* Item 229: an {@code angular} project end to end on the two-workspace fixture of
* {@code ac-parser-typescript} ({@code shop} + {@code lib}): workspaces found without an npm root, components
* with their selector, template calls ({@code TEMPLATE}, origin = the template file), dependency injection
* ({@code INJECTS}), template and stylesheet modules. Needs node + the sidecar's node_modules.
*/
@QuarkusTest
class AngularIT {
private static final String PROJECT = "item229-angular";
@TempDir
static Path root;
@BeforeAll
static void ingest() throws IOException {
assumeTrue(CounterpartsIT.sidecarAvailable(), "node + sidecar/node_modules/typescript not installed here");
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
Path fixture = Path.of("../ac-parser-typescript/src/test/resources/fixtures/angular").toAbsolutePath().normalize();
try (Stream<Path> files = Files.walk(fixture)) {
files.filter(Files::isRegularFile).forEach(f -> {
try {
Path target = root.resolve(fixture.relativize(f).toString());
Files.createDirectories(target.getParent());
Files.copy(f, target);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "angular", null, null))
.when().post("/api/projects/" + PROJECT).then().statusCode(201);
given().when().post("/api/projects/" + PROJECT + "/refresh?deep=true").then().statusCode(200);
}
private static io.restassured.response.Response get(String path) {
return given().when().get("/api/projects/" + PROJECT + path);
}
@Test
void templateUsesAreTemplateCallsFromTheComponent() {
get("/modules/user-badge.component/callers").then().statusCode(200)
.body("items.name", hasItem("shop/src/app/order/order.component"))
.body("items.find { it.name == 'shop/src/app/order/order.component' }.edgeKind", equalTo("TEMPLATE"))
// the call site is in the template file: sites index into sourceFiles
.body("sourceFiles", hasItem("shop/src/app/order/order.component.html"));
get("/modules/spinner.component/callers").then().statusCode(200)
.body("items.name", hasItem("shop/src/app/order/order.component"));
get("/modules/highlight.directive/callers").then().statusCode(200)
.body("items.name", hasItem("shop/src/app/order/order.component"));
}
@Test
void injectedServicesAreInjectsEdges() {
get("/modules/order.component/callees").then().statusCode(200)
// the import is a REFERENCES row of its own; the injection is the INJECTS row
.body("items.findAll { it.name == 'shop/src/app/order/order.service' }.edgeKind", hasItem("INJECTS"))
.body("items.findAll { it.name == 'shop/src/app/order/dialog-helper' }.edgeKind", hasItem("INJECTS"));
}
@Test
void componentsAreFunctionsWithTheirSelector() {
get("/modules/order.component/functions").then().statusCode(200)
.body("find { it.name == 'OrderComponent' }", notNullValue());
get("/search/identifier?name=OrderComponent").then().statusCode(200)
.body("find { it.name == 'OrderComponent' }.type", equalTo("FUNCTION"));
}
@Test
void templatesAndStylesheetsAreModules() {
get("/modules?moduleKind=html").then().statusCode(200)
.body("name", hasItem("shop/src/app/order/order.component.html"));
get("/modules?moduleKind=scss").then().statusCode(200)
.body("name", hasItem("shop/src/app/order/order.component.scss"));
}
/**
* Item 230: routes and dialogs as screens.
*/
@Test
void screensListRoutesAndDialogs() {
get("/screens?kind=route").then().statusCode(200)
.body("name", hasItems("/", "order/:id", "order/:id/badge"))
.body("find { it.name == 'order/:id' }.componentClass", equalTo("OrderComponent"))
.body("find { it.name == 'order/:id' }.guards", equalTo("AuthGuard"))
.body("find { it.name == 'order/:id/badge' }.component", equalTo("shop/src/app/shared/user-badge.component"))
.body("find { it.name == '/' }.redirectTo", equalTo("order"));
get("/screens?kind=dialog").then().statusCode(200)
.body("find { it.name == 'ConfirmDialogComponent' }.openedBy[0].module",
equalTo("shop/src/app/order/order.component"))
.body("find { it.name == 'ConfirmDialogComponent' }.openedBy[0].callerFn", equalTo("OrderComponent"));
get("/screens?component=order.component&countOnly=true").then().statusCode(200)
.body("count", equalTo(1));
get("/screens?kind=popup").then().statusCode(400);
}
/**
* Item 231: form controls are READS + WRITES of the DTO field, sited in the template; grid columns are
* READS only.
*/
@Test
void formFieldsAndGridColumnsAreBindings() {
get("/bindings?dto=AddressDTO&field=street").then().statusCode(200)
.body("mode", containsInAnyOrder("READS", "WRITES"))
.body("[0].function", equalTo("CustomerFormComponent"))
.body("[0].path", equalTo("address.street"))
.body("[0].rootDto", equalTo("CustomerDTO"))
.body("[0].sourceFile", equalTo("shop/src/app/customer/customer-form.component.html"))
.body("[0].lineNo", equalTo(6));
get("/bindings?dto=CustomerRowDTO&field=name").then().statusCode(200)
.body("mode", contains("READS"))
.body("[0].component", equalTo("ag-grid"));
get("/bindings?dto=BaseDTO&field=id&mode=WRITES").then().statusCode(200)
.body("attribute", contains("formControlName"));
// a DTO class: its instance properties (constructor parameter properties too) are fields
get("/bindings?dto=NoteDTO&field=text").then().statusCode(200)
.body("mode", containsInAnyOrder("READS", "WRITES", "READS"));
get("/search/identifier?name=NoteDTO.author").then().statusCode(200)
.body("find { it.name == 'NoteDTO.author' }.type", equalTo("FIELD"));
get("/search/identifier?name=NoteDTO.EMPTY").then().statusCode(200)
.body("$", empty());
}
}

View File

@@ -61,6 +61,43 @@ class DynamicCallOverrideIT {
END
""";
/**
* Item 255: a dynamic call the auto-resolver resolves (literal MOVE) — an override on it is obsolete.
* CALLNAT on line 7.
*/
private static final String AUTODYN = """
DEFINE DATA
LOCAL
01 #P (A8)
END-DEFINE
*
MOVE 'TARGETMOD' TO #P
CALLNAT #P
*
END
""";
/**
* Item 255: the same inside a copycode (CALLNAT on line 3 of AUTOCPY.cpy); the resolved edge hangs on
* the host module, which is found through the copycode nodes' ownerModule.
*/
private static final String AUTOHOST = """
DEFINE DATA
LOCAL
01 #Q (A8)
END-DEFINE
*
INCLUDE AUTOCPY
*
END
""";
private static final String AUTOCPY = """
* copycode with a resolvable dynamic call
MOVE 'TARGET2' TO #Q
CALLNAT #Q
""";
@TempDir
static Path root;
@@ -70,6 +107,9 @@ class DynamicCallOverrideIT {
write("CALLERDYN.nat", CALLERDYN);
write("TARGETMOD.nat", TARGETMOD);
write("TARGET2.nat", TARGET2);
write("AUTODYN.nat", AUTODYN);
write("AUTOHOST.nat", AUTOHOST);
write("AUTOCPY.cpy", AUTOCPY);
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "natural", null, null))
@@ -264,4 +304,27 @@ class DynamicCallOverrideIT {
.then().statusCode(200)
.body("originFile", empty());
}
/**
* Item 255: an override on a call site an auto-resolver has since resolved is listed as obsolete —
* directly in the module and inside a copycode, whose resolved edge hangs on the including host.
*/
@Test
void anOverrideOnAnAutoResolvedSiteIsObsoleteAlsoInACopycode() {
resetAll();
given().pathParam("name", "AUTODYN").when().get("/api/projects/" + PROJECT + "/modules/{name}/callees")
.then().statusCode(200).body("items.name", hasItem("TARGETMOD"));
given().pathParam("name", "AUTOHOST").when().get("/api/projects/" + PROJECT + "/modules/{name}/callees")
.then().statusCode(200).body("items.name", hasItem("TARGET2"));
given().contentType("application/json")
.body(Map.of("originFile", "AUTODYN.nat", "lineNo", 7, "targets", List.of("TARGET2"), "variable", "#P"))
.when().post("/api/projects/" + PROJECT + "/dynamic-calls/overrides").then().statusCode(200);
given().contentType("application/json")
.body(Map.of("originFile", "AUTOCPY.cpy", "lineNo", 3, "targets", List.of("TARGETMOD"), "variable", "#Q"))
.when().post("/api/projects/" + PROJECT + "/dynamic-calls/overrides").then().statusCode(200);
given().when().get("/api/projects/" + PROJECT + "/dynamic-calls/overrides").then().statusCode(200)
.body("find { it.originFile == 'AUTODYN.nat' }.obsolete", equalTo(true))
.body("find { it.originFile == 'AUTOCPY.cpy' }.obsolete", equalTo(true));
resetAll();
}
}

View File

@@ -1,6 +1,9 @@
package com.agenticcode.codeserver.api;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -10,9 +13,11 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Item 203: a caller that invokes an interface at two lines gets one synthetic (CHA) call edge to the
@@ -23,6 +28,9 @@ import static org.hamcrest.Matchers.*;
@QuarkusTest
class InheritanceCallSitesIT {
@Inject
Driver driver;
private static final String PROJECT = "item203-inheritance-sites";
@TempDir
@@ -83,4 +91,23 @@ class InheritanceCallSitesIT {
given().when().get("/api/projects/" + PROJECT + "/modules/com.example.Service/callees").then().statusCode(200)
.body("items.find { it.name == 'com.example.RepoImpl' }.sites.size()", equalTo(2));
}
/**
* Item 204: the hierarchy is resolved where it is read, not materialized. No synthetic
* {@code CALLS} edge exists after a deep refresh, and the module-pair projection still carries
* the implementation — that is where reaches/call-tree get it from.
*/
@Test
void polymorphismIsNotMaterializedAsCallEdges() {
try (Session session = driver.session()) {
long synthetic = session.run(
"MATCH (:AstNode {project: $p})-[r:CALLS {resolvedVia: 'INHERITANCE'}]->() RETURN count(r) AS c",
Map.of("p", PROJECT)).single().get("c").asLong();
assertEquals(0, synthetic, "item 204: polymorphic calls are resolved at query time, never written as edges");
long projected = session.run(
"MATCH (:AstNode {project: $p, name: 'com.example.Service'})-[:CALLS_MODULE]->(:AstNode {name: 'com.example.RepoImpl'}) RETURN count(*) AS c",
Map.of("p", PROJECT)).single().get("c").asLong();
assertEquals(1, projected, "the CALLS_MODULE projection folds the class hierarchy in (item 204)");
}
}
}

View File

@@ -89,4 +89,16 @@ class JavaDbAccessQueryIT {
.then().statusCode(200)
.body("name", hasItem("INVOICES"));
}
}
/**
* Item 236: a table an entity maps ({@code @Table(name = "INVOICES")}) is not flagged unresolved,
* although, like every table, it has no source file.
*/
@Test
void aTableAnEntityMapsIsResolved() {
given().when().get("/api/projects/" + PROJECT + "/search/identifier?name=INVOICES&type=DB_TABLE")
.then().statusCode(200)
.body("size()", org.hamcrest.Matchers.greaterThan(0))
.body("unresolved", org.hamcrest.Matchers.everyItem(org.hamcrest.Matchers.equalTo(false)));
}
}

View File

@@ -36,6 +36,7 @@ class JavaModulesExtendsFilterIT {
copyFixture("fixtures/java/extends-filter/AccountProcessingStep.java");
copyFixture("fixtures/java/extends-filter/RiskProcessingStep.java");
copyFixture("fixtures/java/extends-filter/Unrelated.java");
copyFixture("fixtures/java/extends-filter/PremiumAccountProcessingStep.java");
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "java", null, null))
.when().post("/api/projects/" + PROJECT)
@@ -80,4 +81,25 @@ class JavaModulesExtendsFilterIT {
.body("name", not(hasItem("fixtures.extendsfilter.Unrelated")))
.body("name", not(hasItem("fixtures.extendsfilter.AbstractProcessingStep")));
}
@Test
void directFilterLeavesOutASubclassOfASubclass() {
given().when().get("/api/projects/" + PROJECT + "/modules?extends=AbstractProcessingStep")
.then().statusCode(200)
.body("name", not(hasItem("fixtures.extendsfilter.PremiumAccountProcessingStep")));
}
/**
* Item 209: {@code transitive=true} lists every level below the base. It used to be ignored, so
* the KORIN analysis had to walk 154 subclasses of {@code AbstractWSKService} by hand.
*/
@Test
void transitiveFilterListsEveryLevelBelowTheBase() {
given().when().get("/api/projects/" + PROJECT + "/modules?extends=AbstractProcessingStep&transitive=true")
.then().statusCode(200)
.body("name", hasItems("fixtures.extendsfilter.AccountProcessingStep",
"fixtures.extendsfilter.RiskProcessingStep", "fixtures.extendsfilter.PremiumAccountProcessingStep"))
.body("name", not(hasItem("fixtures.extendsfilter.Unrelated")))
.body("name", not(hasItem("fixtures.extendsfilter.AbstractProcessingStep")));
}
}

View File

@@ -13,6 +13,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasItem;
/**
@@ -110,4 +111,20 @@ class JavaRepositoryOwnTableIT {
.then().statusCode(200)
.body("name", hasItem("RISK"));
}
/**
* Item 211: every db-access row names the module that maps the table, so an agent gets from the
* table to its definition (the {@code @Entity} here, the DDM for Natural/NatJav).
*/
@Test
void dbAccessNamesTheModuleThatMapsTheTable() {
given().pathParam("name", "RiskRepository")
.when().get("/api/projects/" + PROJECT + "/modules/{name}/db-accesses")
.then().statusCode(200)
.body("find { it.name == 'RISK' }.mappedBy", hasItem(containsString("Risk")));
given().pathParam("name", "RiskRepository").queryParam("depth", 1)
.when().get("/api/projects/" + PROJECT + "/modules/{name}/db-accesses")
.then().statusCode(200)
.body("find { it.name == 'RISK' }.mappedBy", hasItem(containsString("Risk")));
}
}

View File

@@ -0,0 +1,199 @@
package com.agenticcode.codeserver.api;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
/**
* KORIN gaps, package B (items 218, 219, 220, 224, 227): fan-in and entry points, the call graph
* rolled up by directory, the table listing, unresolved dynamic calls grouped by variable, the
* index-served exact identifier search (sigil variants included) and annotation attribute values.
*/
@QuarkusTest
class KorinGapsBIT {
private static final String JAVA = "korin-gaps-b-java";
private static final String NATJAV = "korin-gaps-b-natjav";
@TempDir
static Path javaRoot;
@TempDir
static Path natjavRoot;
@BeforeAll
static void ingest() {
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
write(javaRoot, "x/Entry.java", """
package x;
public class Entry {
public void run() { new a.A().go(); }
}
""");
write(javaRoot, "a/A.java", """
package a;
public class A {
public void go() { new b.B().work(); }
}
""");
write(javaRoot, "a/A2.java", """
package a;
public class A2 {
public void go() { new b.B().work(); }
}
""");
write(javaRoot, "b/B.java", """
package b;
public class B {
@WriteThrough(column = "KOS_NAME")
private String name;
@WriteThroughIgnore
private String scratch;
@Column(name = COL)
private String other;
private static final String COL = "OTHER_COL";
public void work() { }
}
""");
write(javaRoot, "e/Risk.java", """
package e;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Entity
@Table(name = "RISK")
public class Risk {
private Long id;
}
""");
write(natjavRoot, "KORIN/Test/XABBRX.java", """
package KORIN.Test;
import innowake.mee.natural.api.*;
@Natural(moduleName="#ABBRX")
public class XABBRX extends NaturalProgram {
@Override
public void run() {
fetch(xWA_PROG);
fetch(xWA_PROG);
callnat(xWA_OTHER);
}
}
""");
for (String[] p : new String[][]{{JAVA, javaRoot.toString(), "java"}, {NATJAV, natjavRoot.toString(), "natjav"}}) {
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, p[1], null, p[2], null, null))
.when().post("/api/projects/" + p[0]).then().statusCode(201);
given().when().post("/api/projects/" + p[0] + "/refresh?deep=true").then().statusCode(200);
}
}
private static void write(Path root, String relative, String content) {
try {
Path file = root.resolve(relative);
Files.createDirectories(file.getParent());
Files.writeString(file, content);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static String api(String project, String path) {
return "/api/projects/" + project + path;
}
@Test
void callerCountAndEntryPoints() {
given().when().get(api(JAVA, "/modules?withCallerCount=true")).then().statusCode(200)
.body("find { it.name == 'b.B' }.callerCount", equalTo(2))
.body("find { it.name == 'x.Entry' }.callerCount", equalTo(0));
given().when().get(api(JAVA, "/modules?noCallers=true")).then().statusCode(200)
.body("name", hasItems("x.Entry", "a.A2"))
.body("name", not(hasItem("b.B")));
given().when().get(api(JAVA, "/modules?sort=fanIn")).then().statusCode(200)
.body("[0].name", equalTo("b.B"));
given().when().get(api(JAVA, "/modules")).then().statusCode(200)
.body("[0].callerCount", nullValue());
given().when().get(api(JAVA, "/modules?sort=size")).then().statusCode(400)
.body("code", equalTo("SORT_UNSUPPORTED"));
}
@Test
void callGraphRolledUpByDirectory() {
given().when().get(api(JAVA, "/call-graph/aggregate?groupBy=path&level=1")).then().statusCode(200)
.body("find { it.fromGroup == 'a' && it.toGroup == 'b' }.calls", equalTo(2))
.body("find { it.fromGroup == 'a' && it.toGroup == 'b' }.callers", equalTo(2))
.body("find { it.fromGroup == 'x' && it.toGroup == 'a' }.calls", equalTo(1));
given().when().get(api(JAVA, "/call-graph/aggregate?groupBy=owner")).then().statusCode(400);
}
@Test
void dbTablesListTheMappingModule() {
given().when().get(api(JAVA, "/db-tables?stats=true")).then().statusCode(200)
.body("find { it.name == 'RISK' }.mappedBy", hasItem("e.Risk"))
.body("find { it.name == 'RISK' }.readers", equalTo(0));
given().when().get(api(JAVA, "/db-tables")).then().statusCode(200)
.body("find { it.name == 'RISK' }.readers", nullValue());
}
@Test
void unresolvedDynamicCallsHaveTheirStatementAndGroup() {
given().when().get(api(NATJAV, "/dynamic-calls/unresolved")).then().statusCode(200)
.body("findAll { it.variable == 'xWA_PROG' }.statementKind", everyItem(equalTo("FETCH")))
.body("find { it.variable == 'xWA_OTHER' }.statementKind", equalTo("CALLNAT"));
given().when().get(api(NATJAV, "/dynamic-calls/unresolved?groupBy=variable")).then().statusCode(200)
.body("[0].variable", equalTo("xWA_PROG"))
.body("[0].sites", equalTo(2))
.body("[0].statementKinds", contains("FETCH"));
}
@Test
void exactIdentifierSearchFindsSigilVariantsAndShortNames() {
given().when().get(api(NATJAV, "/search/identifier?name=ABBRX")).then().statusCode(200)
.body("name", hasItem("#ABBRX"));
given().when().get(api(JAVA, "/search/identifier?name=B&type=MODULE")).then().statusCode(200)
.body("name", contains("b.B"));
given().when().get(api(JAVA, "/search/identifier?name=b.B")).then().statusCode(200)
.body("name", hasItem("b.B"));
}
@Test
void annotationAttributesAreListedAndFilterable() {
given().when().get(api(JAVA, "/search/annotation?name=WriteThrough&exact=true")).then().statusCode(200)
.body("name", contains("name"))
.body("[0].annotationList[0].name", equalTo("WriteThrough"))
.body("[0].annotationList[0].attributes.column", equalTo("KOS_NAME"));
given().when().get(api(JAVA, "/search/annotation?name=WriteThrough&attribute=column&value=KOS_NAME")).then()
.statusCode(200)
.body("name", contains("name"));
given().when().get(api(JAVA, "/search/annotation?name=Column&value=OTHER_COL")).then().statusCode(200)
.body("name", contains("other"));
given().when().get(api(JAVA, "/search/annotation?name=WriteThrough&value=NOPE")).then().statusCode(200)
.body("$", empty());
}
/**
* Item 240: {@code /modules} honours countOnly, limit and offset; without them it stays the full list.
*/
@Test
void modulesCountOnlyAndPaging() {
int all = given().when().get(api(JAVA, "/modules")).then().statusCode(200)
.extract().jsonPath().getList("$").size();
org.junit.jupiter.api.Assertions.assertTrue(all > 1, "the fixture has several modules");
given().when().get(api(JAVA, "/modules?countOnly=true")).then().statusCode(200)
.body("count", equalTo(all));
given().when().get(api(JAVA, "/modules?limit=1")).then().statusCode(200)
.header("X-AC-Truncated", "true")
.body("size()", equalTo(1));
given().when().get(api(JAVA, "/modules?limit=1&offset=" + (all - 1))).then().statusCode(200)
.header("X-AC-Truncated", "false")
.body("size()", equalTo(1));
}
}

View File

@@ -0,0 +1,248 @@
package com.agenticcode.codeserver.api;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
/**
* KORIN gaps, package C (items 225, 226): a service project (Axis2 {@code services.xml} + skeleton +
* configuration with a password) and a client project (a WSDL for the same service, a JAX-WS class).
* {@code soap-endpoints} lists all three sources, the client's WSDL is the counterpart of the Axis2
* service, configuration entries are searchable, and no password reaches the graph.
*/
@QuarkusTest
class KorinGapsCIT {
private static final String SERVICE = "korin-gaps-c-service";
private static final String CLIENT = "korin-gaps-c-client";
@TempDir
static Path serviceRoot;
@TempDir
static Path clientRoot;
@BeforeAll
static void ingest() {
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
write(serviceRoot, "WebContent/WEB-INF/services/WS_Leitwaehrung/META-INF/services.xml", """
<?xml version="1.0" encoding="UTF-8"?>
<serviceGroup>
<service name="WS_Leitwaehrung">
<parameter name="ServiceClass">com.x.mapping.WS_LeitwaehrungSkeleton</parameter>
<operation name="getData">
<actionMapping>getData</actionMapping>
<outputActionMapping>http://namespace.softwareag.com/entirex/xml/mapping/WSK386Port/getDataResponse</outputActionMapping>
</operation>
</service>
</serviceGroup>
""");
write(serviceRoot, "src/com/x/mapping/WS_LeitwaehrungSkeleton.java", """
package com.x.mapping;
public class WS_LeitwaehrungSkeleton {
public void getData() { }
}
""");
write(serviceRoot, "res/natparm/localkos.properties", """
default.Database.1=URL=jdbc:sqlserver://attskosdb1:1433;databaseName=KOS4;password=geheim
default.Database.3=Password=IWdataGRA14!
""");
write(serviceRoot, "src/META-INF/persistence.xml", """
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="3.0">
<persistence-unit name="KOS">
<jta-data-source>java:/KOSDB</jta-data-source>
</persistence-unit>
</persistence>
""");
write(serviceRoot, "pom.xml", "<project><artifactId>not-config</artifactId></project>");
write(serviceRoot, "src/main/resources/kosDataAccessDBConfiguration.xml", """
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="kosEmf">
<property name="persistenceUnitName" value="KOSDB"/>
</bean>
</beans>
""");
write(serviceRoot, "testdata/fixture.xml", "<data><row>KOSDB-NOT-CONFIG</row></data>");
write(serviceRoot, "src/main/resources/data/VERSICHERER_1.xml", "<?xml version=\"1.0\"?>\n<Kunden><Kunde>X</Kunde></Kunden>");
write(serviceRoot, "src/main/resources/Translations_de.properties", "label.ok=OK\n");
write(serviceRoot, "src/test/resources/test.properties", "db.url=jdbc:test\n");
write(clientRoot, "src/main/resources/wsdl/korin/WS_Leitwaehrung.wsdl", """
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="urn:x"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<wsdl:types><xsd:schema targetNamespace="urn:com-softwareag-entirex-rpc:WSK386"/></wsdl:types>
<wsdl:portType name="WSK386Port"><wsdl:operation name="getData"/></wsdl:portType>
<wsdl:binding name="B" type="tns:WSK386Port"><soap:binding style="document"/></wsdl:binding>
<wsdl:service name="WS_Leitwaehrung">
<wsdl:port name="Port" binding="tns:B"><soap:address location="http://korin/WS_Leitwaehrung"/></wsdl:port>
</wsdl:service>
</wsdl:definitions>
""");
write(clientRoot, "src/main/resources/wsdl/korin/WS_Leitwaehrung-binding.xml", """
<bindings wsdlLocation="WS_Leitwaehrung.wsdl" xmlns="http://java.sun.com/xml/ns/jaxws">
<package name="at.k3.entirex.ws.wsk386" />
<enableWrapperStyle>false</enableWrapperStyle>
</bindings>
""");
write(clientRoot, "src/main/resources/wsdl/korin/WS_Leitwaehrung-interface-binding.xml", """
<bindings xmlns="https://jakarta.ee/xml/ns/jaxb" version="3.0">
<bindings schemaLocation="WS_Leitwaehrung.wsdl"><class name="X"/></bindings>
</bindings>
""");
write(clientRoot, "src/main/java/at/k3/base/ws/wsk386/WSK386GetDataService.java", """
package at.k3.base.ws.wsk386;
import at.k3.entirex.ws.wsk386.WSLeitwaehrung;
public class WSK386GetDataService {
public Class<?> port() {
return WSLeitwaehrung.class;
}
}
""");
// item 258: a class that only uses the request type JAXB generates from the WSDL's schema
write(clientRoot, "src/main/java/at/k3/base/ws/wsk386/WSK386Request.java", """
package at.k3.base.ws.wsk386;
import rpc.entirex.softwareag.com.wsk386.GetData;
public class WSK386Request {
public GetData build() {
return new GetData();
}
}
""");
write(clientRoot, "src/main/java/at/k3/ws/Stete.java", """
package at.k3.ws;
import jakarta.jws.WebMethod;
import jakarta.jws.WebService;
@WebService(serviceName = "SteteService")
public class Stete {
@WebMethod(operationName = "melde")
public void report() { }
public void helper() { }
}
""");
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, serviceRoot.toString(), null, "java", null, null))
.when().post("/api/projects/" + SERVICE).then().statusCode(201);
given().when().post("/api/projects/" + SERVICE + "/refresh?deep=true").then().statusCode(200);
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, clientRoot.toString(), null, "java", null, null,
List.of(SERVICE)))
.when().post("/api/projects/" + CLIENT).then().statusCode(201);
given().when().post("/api/projects/" + CLIENT + "/refresh?deep=true").then().statusCode(200);
}
private static void write(Path root, String relative, String content) {
try {
Path file = root.resolve(relative);
Files.createDirectories(file.getParent());
Files.writeString(file, content);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static String api(String project, String path) {
return "/api/projects/" + project + path;
}
@Test
void axis2ServiceNamesItsSkeleton() {
given().when().get(api(SERVICE, "/soap-endpoints")).then().statusCode(200)
.body("find { it.service == 'WS_Leitwaehrung' }.kind", equalTo("axis2"))
.body("find { it.service == 'WS_Leitwaehrung' }.operation", equalTo("getData"))
.body("find { it.service == 'WS_Leitwaehrung' }.implementation",
equalTo("com.x.mapping.WS_LeitwaehrungSkeleton"));
}
@Test
void clientWsdlIsTheCounterpartOfTheService() {
given().when().get(api(CLIENT, "/soap-endpoints?kind=wsdl")).then().statusCode(200)
.body("[0].service", equalTo("WS_Leitwaehrung"))
.body("[0].location", equalTo("http://korin/WS_Leitwaehrung"))
.body("[0].counterpartProject", equalTo(SERVICE))
.body("[0].counterpartService", equalTo("WS_Leitwaehrung"));
given().when().get(api(CLIENT, "/counterparts?kind=soap")).then().statusCode(200)
.body("[0].counterpartModule", equalTo("WS_Leitwaehrung"));
}
@Test
void jaxwsClassListsItsWebMethodsOnly() {
given().when().get(api(CLIENT, "/soap-endpoints?kind=jaxws")).then().statusCode(200)
.body("service", everyItem(equalTo("SteteService")))
.body("operation", contains("melde"))
.body("implementation", everyItem(equalTo("at.k3.ws.Stete")));
}
@Test
void configurationIsSearchableAndSecretsAreMasked() {
given().when().get(api(SERVICE, "/search/value?value=java:/KOSDB")).then().statusCode(200)
.body("name", hasItem("persistence/persistence-unit[KOS]/jta-data-source"));
given().when().get(api(SERVICE, "/search/value?value=KOS4&contains=true")).then().statusCode(200)
.body("value", hasItem(containsString("password=***")));
given().when().get(api(SERVICE, "/search/value?value=geheim&contains=true")).then().statusCode(200)
.body("$", empty());
given().when().get(api(SERVICE, "/search/value?value=IWdataGRA14&contains=true")).then().statusCode(200)
.body("$", empty());
given().when().get(api(SERVICE, "/modules?moduleKind=CONFIG")).then().statusCode(200)
.body("name", hasItems("res/natparm/localkos.properties", "src/META-INF/persistence.xml",
"src/main/resources/kosDataAccessDBConfiguration.xml"))
.body("name", not(hasItem("pom.xml")))
.body("name", not(hasItem("testdata/fixture.xml")))
.body("name", not(hasItem("src/main/resources/data/VERSICHERER_1.xml")))
.body("name", not(hasItem("src/main/resources/Translations_de.properties")))
.body("name", not(hasItem("src/test/resources/test.properties")));
given().when().get(api(SERVICE, "/search/value?value=KOSDB")).then().statusCode(200)
.body("name", hasItem("beans/bean[kosEmf]/property[persistenceUnitName]@value"));
}
/**
* Item 238: the EntireX port is read from the Axis2 action mapping and from the WSDL portType, and
* {@code ?port=} finds both.
*/
@Test
void portNameFindsTheServiceOnBothSides() {
given().when().get(api(SERVICE, "/soap-endpoints?port=wsk386")).then().statusCode(200)
.body("service", everyItem(equalTo("WS_Leitwaehrung")))
.body("port", everyItem(equalTo("WSK386")))
.body("size()", greaterThan(0));
given().when().get(api(CLIENT, "/soap-endpoints?kind=wsdl&port=WSK386")).then().statusCode(200)
.body("[0].service", equalTo("WS_Leitwaehrung"));
given().when().get(api(CLIENT, "/soap-endpoints?port=WSK999")).then().statusCode(200)
.body("$", empty());
}
/**
* Item 239: the wsimport binding names the package of the WSDL's generated client; the WSDL row carries
* it and lists the modules importing that package. The JAXB interface binding is no configuration.
*/
@Test
void wsimportBindingLinksTheGeneratedClientToItsWsdl() {
given().when().get(api(CLIENT, "/soap-endpoints?kind=wsdl&service=WS_Leitwaehrung")).then().statusCode(200)
.body("[0].clientPackage", equalTo("at.k3.entirex.ws.wsk386"))
// item 258: the user of the schema's JAXB types is a client too
.body("[0].clients", containsInAnyOrder("at.k3.base.ws.wsk386.WSK386GetDataService",
"at.k3.base.ws.wsk386.WSK386Request"))
.body("[0].typesPackages", equalTo("rpc.entirex.softwareag.com.wsk386"))
.body("[0].counterpartProject", equalTo(SERVICE));
given().when().get(api(CLIENT, "/modules?moduleKind=WSIMPORT_BINDING")).then().statusCode(200)
.body("name", contains("src/main/resources/wsdl/korin/WS_Leitwaehrung-binding.xml"));
given().when().get(api(CLIENT, "/modules?moduleKind=CONFIG")).then().statusCode(200)
.body("name", not(hasItem("src/main/resources/wsdl/korin/WS_Leitwaehrung-interface-binding.xml")));
}
}

View File

@@ -0,0 +1,201 @@
package com.agenticcode.codeserver.api;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
/**
* Item 236: a NatJav table is described by its DDM, and the DDM points into an FDT. {@code /db-tables}
* names the FDT and the ADABAS file number, {@code /db-tables/{name}/columns} and
* {@code /modules/{ddm}/columns} list the DDM columns with the FDT's length and options, and the table is
* no longer flagged unresolved.
*/
@QuarkusTest
class NatJavAdabasFileIT {
private static final String PROJECT = "item236-natjav";
private static final String NATURAL = "item236-natural";
private static final Path FIXTURES = Path.of("../ac-parser-natjav/src/test/resources/fixtures/natjav")
.toAbsolutePath().normalize();
@TempDir
static Path root;
@TempDir
static Path naturalRoot;
@BeforeAll
static void ingest() throws IOException {
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
copy("MDUMKONT_DDM.java", "SYSTEM/MDUMKONT_DDM.java");
copy("MDBT74B.java", "KORIN/Mitarbeiter/Verwaltung/MDBT74B.java");
// item 257: a data area whose view carries the data area's own name, like korin's YLAKUNDE
Path area = root.resolve("KORIN/Programmierer/YLMDUM_DA.java");
Files.createDirectories(area.getParent());
Files.writeString(area, """
package KORIN.Programmierer;
import innowake.mee.natural.api.*;
@Natural(moduleName="YLMDUM")
public class YLMDUM_DA extends NaturalData {
@Local
@L1 @ViewOf SYSTEM.MDUMKONT_DDM YLMDUM = view("YLMDUM");
@L2 A SATZART;
@L2 P VKTO;
}
""");
// item 256: a program that writes a column through its view
Path writer = root.resolve("KORIN/Mitarbeiter/Verwaltung/MDWRITE.java");
Files.writeString(writer, """
package KORIN.Mitarbeiter.Verwaltung;
import innowake.mee.natural.api.*;
@Natural
public class MDWRITE extends NaturalProgram {
@Local
@L1 A xWA_ART = A(1 ,"#WA-ART");
@L1 @ViewOf SYSTEM.MDUMKONT_DDM WRK = view("WRK");
@L2 A SATZART;
@L2 A SPARTEN;
@Override
public void run() {
assign(WRK.SATZART, "M");
assign(xWA_ART, WRK.SATZART);
reset(WRK.SPARTEN);
}
}
""");
Path fdt = root.resolve("database/FDT/FDT0022.java");
Files.createDirectories(fdt.getParent());
Files.writeString(fdt, """
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package database.FDT;
import innowake.mee.natural.api.*;
@Natural
public class FDT0022 extends NaturalFdt {
/* Field information from the parsing of FDT File: F22
*
* Shortname: AA, DataType: A, Length: 1, Options: FI
* Shortname: AY, DataType: P, Length: 4, Options: DE NU
* Shortname: CC, DataType: P, Length: 2, Options: NU
*/
public static final DbA AA = DbA(1, "AA");
public static final DbP AY = DbP(7, 0, "AY");
public static final DbP CC = DbP(3, 0, "CC");
}
""");
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "natjav", null, null))
.when().post("/api/projects/" + PROJECT).then().statusCode(201);
given().when().post("/api/projects/" + PROJECT + "/refresh?deep=true").then().statusCode(200);
// a Natural program reading a view no DDM in the project describes — nothing maps that table
Path program = naturalRoot.resolve("GHOSTRD.nat");
Files.writeString(program, """
DEFINE DATA LOCAL
1 GHOST-VIEW VIEW OF GHOST
2 NAME (A20)
END-DEFINE
READ GHOST-VIEW BY NAME
END-READ
END
""");
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, naturalRoot.toString(), null, "natural", null, null))
.when().post("/api/projects/" + NATURAL).then().statusCode(201);
given().when().post("/api/projects/" + NATURAL + "/refresh?deep=true").then().statusCode(200);
}
private static void copy(String fixture, String target) throws IOException {
Path file = root.resolve(target);
Files.createDirectories(file.getParent());
Files.copy(FIXTURES.resolve(fixture), file);
}
private static io.restassured.response.Response get(String path) {
return given().when().get("/api/projects/" + PROJECT + path);
}
@Test
void tableNamesItsFdtAndFileNumber() {
get("/db-tables?name=MDUMKONT").then().statusCode(200)
.body("find { it.name == 'MDUMKONT' }.mappedBy", contains("MDUMKONT"))
.body("find { it.name == 'MDUMKONT' }.fdt", equalTo("FDT0022"))
.body("find { it.name == 'MDUMKONT' }.fileNumber", equalTo("22"));
}
@Test
void columnsComeFromTheDdmWithFdtLengthAndOptions() {
get("/db-tables/MDUMKONT/columns").then().statusCode(200)
.body("size()", equalTo(25))
// item 257: the data area's view is not a column of the table
.body("type", everyItem(equalTo("COLUMN")))
.body("[0].name", equalTo("SATZART"))
.body("find { it.name == 'SATZART' }.adabasShortName", equalTo("AA"))
.body("find { it.name == 'SATZART' }.dataType", equalTo("A1"))
.body("find { it.name == 'SATZART' }.adabasOptions", equalTo("FI"))
.body("find { it.name == 'SATZART' }.parent", equalTo("MDUMKONT"))
.body("find { it.name == 'SATZART' }.fdt", equalTo("FDT0022"))
.body("find { it.name == 'VKTO' }.dataType", equalTo("P7"))
.body("find { it.name == 'VKTO' }.adabasOptions", equalTo("DE NU"))
// not in the FDT: the DDM's own format stays
.body("find { it.name == 'VR-KEY' }.dataType", equalTo("P3"))
// neither in the FDT nor with a length in the DDM: only the DDM's format letter
.body("find { it.name == 'ANLAGE-VON' }.dataType", equalTo("A"))
// item 256: statements through every view of the table — MDBT74B reads SPARTEN through
// both of its views (UMK, DEL aliases), MDWRITE writes and reads SATZART and resets SPARTEN
.body("find { it.name == 'SPARTEN' }.reads", equalTo(2))
.body("find { it.name == 'SPARTEN' }.writes", equalTo(1))
.body("find { it.name == 'SATZART' }.reads", equalTo(1))
.body("find { it.name == 'SATZART' }.writes", equalTo(1))
.body("find { it.name == 'VKTO' }.writes", equalTo(0));
// the sites themselves: the column's name reaches the view fields
get("/variables/SATZART/writes?module=MDWRITE").then().statusCode(200)
.body("size()", greaterThanOrEqualTo(1));
get("/modules/MDUMKONT/columns").then().statusCode(200)
.body("find { it.columnName == 'VKTO' }.attributeName", equalTo("VKTO"))
.body("find { it.columnName == 'VKTO' }.columnDefinition", equalTo("P7"))
.body("find { it.columnName == 'VKTO' }.declaredIn", equalTo("MDUMKONT"));
}
@Test
void fdtFieldsAreSearchable() {
get("/search/identifier?name=AY").then().statusCode(200)
.body("find { it.sourceFile == 'database/FDT/FDT0022.java' }.type", equalTo("FIELD"))
.body("find { it.sourceFile == 'database/FDT/FDT0022.java' }.dataType", equalTo("P7"));
}
/**
* A table a DDM maps is resolved. NatJav creates no table without a DDM, so the opposite case — a table
* nothing maps — comes from a Natural {@code READ} on a view of an undescribed file.
*/
@Test
void aMappedTableIsResolvedAnUnmappedOneIsNot() {
get("/search/identifier?name=MDUMKONT&type=DB_TABLE").then().statusCode(200)
.body("size()", greaterThan(0))
.body("unresolved", everyItem(equalTo(false)));
given().when().get("/api/projects/" + NATURAL + "/search/identifier?name=GHOST&type=DB_TABLE").then()
.statusCode(200)
.body("size()", greaterThan(0))
.body("unresolved", everyItem(equalTo(true)));
}
}

View File

@@ -0,0 +1,197 @@
package com.agenticcode.codeserver.api;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
/**
* Item 214: a Java web-service layer (the KORIN Axis2 skeletons) reaches NatJav programs through the
* runtime — {@code WSX390UD pgm = serviceCaller.getObject("KORIN.Diverse.Webservices.WSX390UD")}.
* With the NatJav project as counterpart, the qualified-name placeholder is linked
* {@code COUNTERPART_OF {via: natjav}} to the program, {@code counterparts?kind=natjav} lists skeleton
* → program, {@code counterpartModule=} answers "which skeleton calls facade X", and a reference to a
* class outside every NatJav package ({@code java.util.HashMap}) is no NatJav row at all.
*/
@QuarkusTest
class NatJavCounterpartsIT {
private static final String NATJAV = "item214-natjav";
private static final String WS = "item214-ws";
@TempDir
static Path natjavRoot;
@TempDir
static Path wsRoot;
@BeforeAll
static void ingest() {
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
Path lib = natjavRoot.resolve("KORIN/Diverse/Webservices");
write(lib, "WSX390UD.java", """
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN.Diverse.Webservices;
import innowake.mee.natural.api.*;
@Natural
public class WSX390UD extends NaturalSubprogram {
@Override
public void run() {
callnat("WSX390XX");
}
}
""");
write(lib, "WSX391UD.java", """
package KORIN.Diverse.Webservices;
import innowake.mee.natural.api.*;
@Natural
public class WSX391UD extends NaturalSubprogram {
@Override
public void run() {
}
}
""");
Path mapping = wsRoot.resolve("com/softwareag/namespace/entirex/xml/mapping");
write(mapping, "WS_X390Skeleton.java", """
package com.softwareag.namespace.entirex.xml.mapping;
import java.util.HashMap;
import KORIN.Diverse.Webservices.WSX390UD;
import KORIN.Diverse.Webservices.WSXGONE;
public class WS_X390Skeleton {
private final ServiceCaller serviceCaller = new ServiceCaller();
public void updateData() {
HashMap<String, String> map = new HashMap<>();
WSX390UD pgm = serviceCaller.getObject("KORIN.Diverse.Webservices.WSX390UD");
serviceCaller.callnat(pgm.getClass().getSimpleName(), map);
}
public void getData() {
WSXGONE gone = serviceCaller.getObject("KORIN.Diverse.Webservices.WSXGONE");
serviceCaller.callnat(gone.getClass().getSimpleName());
}
}
""");
write(mapping, "ServiceCaller.java", """
package com.softwareag.namespace.entirex.xml.mapping;
public class ServiceCaller {
public <T> T getObject(String name) {
return null;
}
public void callnat(String name, Object... args) {
}
}
""");
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, natjavRoot.toString(), null, "natjav", null, null))
.when().post("/api/projects/" + NATJAV).then().statusCode(201);
given().when().post("/api/projects/" + NATJAV + "/refresh?deep=true").then().statusCode(200);
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, wsRoot.toString(), null, "java", null, null,
List.of(NATJAV)))
.when().post("/api/projects/" + WS).then().statusCode(201);
given().when().post("/api/projects/" + WS + "/refresh?deep=true").then().statusCode(200);
}
private static void write(Path dir, String fileName, String content) {
try {
Files.createDirectories(dir);
Files.writeString(dir.resolve(fileName), content);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static io.restassured.response.Response counterparts(String query) {
return given().when().get("/api/projects/" + WS + "/counterparts" + query);
}
@Test
void skeletonCallIsLinkedToTheNatJavProgram() {
counterparts("?kind=natjav").then().statusCode(200)
.body("find { it.name == 'KORIN.Diverse.Webservices.WSX390UD' }.module",
equalTo("com.softwareag.namespace.entirex.xml.mapping.WS_X390Skeleton"))
.body("find { it.name == 'KORIN.Diverse.Webservices.WSX390UD' }.counterpartProject", equalTo(NATJAV))
.body("find { it.name == 'KORIN.Diverse.Webservices.WSX390UD' }.counterpartModule", equalTo("WSX390UD"))
.body("find { it.name == 'KORIN.Diverse.Webservices.WSX390UD' }.counterpartSourceFile",
equalTo("KORIN/Diverse/Webservices/WSX390UD.java"));
}
/**
* A class name outside every NatJav package is not a NatJav call — it must not show up as an
* unmatched row, or {@code unmatched=true} would list every {@code java.util} reference.
*/
@Test
void onlyNatJavPackagesAreNatJavRows() {
counterparts("?kind=natjav").then().statusCode(200)
.body("name", not(hasItem("java.util.HashMap")))
.body("kind", everyItem(equalTo("natjav")));
counterparts("?kind=natjav&unmatched=true").then().statusCode(200)
.body("name", contains("KORIN.Diverse.Webservices.WSXGONE"));
}
@Test
void counterpartModuleAnswersWhichSkeletonCallsAFacade() {
counterparts("?counterpartModule=WSX390UD").then().statusCode(200)
.body("module", contains("com.softwareag.namespace.entirex.xml.mapping.WS_X390Skeleton"));
counterparts("?counterpartModule=WSX391UD").then().statusCode(200)
.body("$", empty());
}
@Test
void unknownKindStillRejected() {
counterparts("?kind=graphql").then().statusCode(400).body("code", equalTo("KIND_UNSUPPORTED"));
}
/**
* A refresh of the NatJav side recreates the program node and drops the edges into it; the
* counterpart links are rebuilt for every project that names it.
*/
@Test
void refreshingTheNatJavSideKeepsTheLink() {
given().when().post("/api/projects/" + NATJAV + "/refresh?deep=true").then().statusCode(200);
counterparts("?counterpartModule=WSX390UD").then().statusCode(200)
.body("module", contains("com.softwareag.namespace.entirex.xml.mapping.WS_X390Skeleton"));
}
/**
* Item 237: callees of the skeleton name the NatJav twin of the unresolved facade class, and the facade's
* callers in the NatJav project include the skeleton from the web-service project.
*/
@Test
void calleesAndCallersShowTheCrossProjectStep() {
String skeleton = "com.softwareag.namespace.entirex.xml.mapping.WS_X390Skeleton";
given().when().get("/api/projects/" + WS + "/modules/" + skeleton + "/callees").then().statusCode(200)
.body("items.find { it.name == 'KORIN.Diverse.Webservices.WSX390UD' }.unresolved", equalTo(true))
.body("items.find { it.name == 'KORIN.Diverse.Webservices.WSX390UD' }.counterpartProject", equalTo(NATJAV))
.body("items.find { it.name == 'KORIN.Diverse.Webservices.WSX390UD' }.counterpartModule", equalTo("WSX390UD"))
.body("items.find { it.name == 'KORIN.Diverse.Webservices.WSXGONE' }.counterpartProject", nullValue());
given().when().get("/api/projects/" + NATJAV + "/modules/WSX390UD/callers").then().statusCode(200)
.body("items.find { it.name == '" + skeleton + "' }.project", equalTo(WS))
.body("sourceFiles", hasItem("com/softwareag/namespace/entirex/xml/mapping/WS_X390Skeleton.java"));
given().when().get("/api/projects/" + NATJAV + "/modules/WSX391UD/callers").then().statusCode(200)
.body("items", empty());
}
}

View File

@@ -0,0 +1,186 @@
package com.agenticcode.codeserver.api;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* The first NatJav end-to-end test (roadmap item 247): a six-file NatJav project goes through
* project creation, a whole-root deep refresh and the REST API. Until this class existed, every
* NatJav behaviour was verified by parser unit tests and by the live reference corpus only.
*
* <p>The root reproduces the corpus's shape where it matters: libraries are top-level directories,
* {@code COMMON_DA.java} exists in two of them ({@code KORIN}, {@code ABFRAGE}), and a program in a
* third library ({@code CSTENTW}) includes the KORIN one — the case a directory heuristic would bind
* wrong and the {@code @Using} qualifier binds right (item 244). One assertion per fix it protects.
*/
@QuarkusTest
class NatJavIngestIT {
private static final String PROJECT = "natjav-it";
private static final String API = "/api/projects/" + PROJECT;
@TempDir
static Path root;
@BeforeAll
static void ingest() {
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
// Libraries are directories; the same class name in two of them is the homonym under test.
copyFixture("fixtures/natjav/COMMON_DA.java", "KORIN/COMMON_DA.java");
copyFixture("fixtures/natjav/abfrage/COMMON_DA.java", "ABFRAGE/COMMON_DA.java");
copyFixture("fixtures/natjav/V_MIT04.java", "KORIN/Mitarbeiter/Verwaltung/V_MIT04.java");
copyFixture("fixtures/natjav/YPUMPRUE_DA.java", "KORIN/Mitarbeiter/Verwaltung/YPUMPRUE_DA.java");
copyFixture("fixtures/natjav/UMKTEST.java", "KORIN/UMKTEST.java");
copyFixture("fixtures/natjav/DISKTEST.java", "CSTENTW/DISKTEST.java");
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "natjav", null, null))
.when().post(API)
.then().statusCode(201);
deepRefresh();
}
private static void deepRefresh() {
given().when().post(API + "/refresh?deep=true").then().statusCode(200);
}
/**
* Unlike the Natural fixtures, these need their directory: the top-level one is the library, which
* {@code NatJavIdentities} qualifies homonyms by.
*/
private static void copyFixture(String classpathResource, String relativeTarget) {
try (InputStream in = NatJavIngestIT.class.getClassLoader().getResourceAsStream(classpathResource)) {
if (in == null) {
throw new IllegalStateException("Resource not found: " + classpathResource);
}
Path target = root.resolve(relativeTarget);
Files.createDirectories(target.getParent());
Files.write(target, in.readAllBytes());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static String dataStructures(String module) {
return given().when().get(API + "/modules/" + module + "/data-structures")
.then().statusCode(200).extract().asString();
}
/**
* Item A: the module answers to its Natural name, not to the Java class name it was parsed from.
* Looked up by file — there is no bare {@code GET /modules/{name}}, only sub-resources — and then
* addressed by that name, which is what every sub-resource call below relies on.
*/
@Test
void moduleIsAddressedByItsNaturalName() {
given().when().get(API + "/modules?sourceFile=KORIN/Mitarbeiter/Verwaltung/V_MIT04.java")
.then().statusCode(200)
.body("size()", equalTo(1))
.body("[0].name", equalTo("V-MIT04"))
.body("[0].moduleKind", equalTo("SUBPROGRAM"));
given().when().get(API + "/modules/V-MIT04/functions")
.then().statusCode(200);
}
/**
* Item 244: {@code COMMON_DA} has two real candidates. A module in a THIRD library includes
* {@code KORIN.COMMON_DA}, and the qualifier — not the module's own directory — decides.
*/
@Test
void homonymResolvesToTheLibraryTheUsingNames() {
given().when().get(API + "/modules/DISKTEST/data-structures")
.then().statusCode(200)
.body("find { it.name == 'KORIN.COMMON' }.relationship", equalTo("USING"))
.body("find { it.name == 'KORIN.COMMON' }.area", equalTo("GDA"))
.body("find { it.name == 'KORIN.COMMON' }.sourceFile", equalTo("KORIN/COMMON_DA.java"))
.body("find { it.name == 'KORIN.COMMON' }.fieldCount", greaterThan(0))
.body("findAll { it.name == 'ABFRAGE.COMMON' }", hasSize(0))
.body("findAll { it.name == 'COMMON_DA' }", hasSize(0));
}
/**
* Item 244, the unqualified form: {@code @Using COMMON_DA} carries no package, so only Java's
* same-package rule — the including file's own {@code package KORIN;} — can pick a candidate.
* With one candidate the class name alone would do; this needs the homonym to be load-bearing.
*/
@Test
void unqualifiedUsingResolvesThroughTheFilesOwnPackage() {
given().when().get(API + "/modules/UMKTEST/data-structures")
.then().statusCode(200)
.body("find { it.name == 'KORIN.COMMON' }.area", equalTo("GDA"))
.body("findAll { it.name == 'ABFRAGE.COMMON' }", hasSize(0))
.body("find { it.name == 'YPUMPRUE' }.area", equalTo("LDA"));
}
/**
* Item 246: {@code area} comes from the {@code @Using} scope on the INCLUDES edge, not from a file
* extension a {@code .java} area does not have — and only for a resolved definition.
*/
@Test
void areaComesFromTheUsingScopeOnResolvedRowsOnly() {
given().when().get(API + "/modules/DISKTEST/data-structures")
.then().statusCode(200)
.body("find { it.name == 'YPUMPRUE' }.area", equalTo("LDA"))
.body("find { it.name == 'YPUMPRUE' }.sourceFile", equalTo("KORIN/Mitarbeiter/Verwaltung/YPUMPRUE_DA.java"))
.body("find { it.name == 'YPUMPRUE' }.fieldCount", greaterThan(0))
// Unresolved: the file is not in the project, so UNKNOWN keeps meaning exactly that.
.body("find { it.name == 'NOFILE_DA' }.area", equalTo("UNKNOWN"))
.body("find { it.name == 'NOFILE_DA' }.sourceFile", nullValue())
.body("find { it.name == 'NOFILE_DA' }.fieldCount", equalTo(0));
// A PARAMETER include whose area is not in the project is unresolved too - scope alone must
// not promote it to PDA.
given().when().get(API + "/modules/V-MIT04/data-structures")
.then().statusCode(200)
.body("find { it.name == 'GWSTGP_DA' }.area", equalTo("UNKNOWN"))
.body("find { it.name == 'GWSTGP_DA' }.sourceFile", nullValue());
}
/** Item B: {@code callnat("MITZ1R", ...)} is a CALLNAT to a module the project does not hold. */
@Test
void callnatTargetIsAnUnresolvedCallee() {
given().when().get(API + "/modules/V-MIT04/callees")
.then().statusCode(200)
.body("items.find { it.name == 'MITZ1R' }.unresolved", equalTo(true))
.body("items.find { it.name == 'MITZ1R' }.edgeKind", equalTo("CALLNAT"));
}
/**
* Item 243: a second deep refresh over unchanged input must leave the graph as it found it. The
* leak this guards against minted one placeholder DATA_STRUCTURE per ingest, which the identifier
* search would show as a second hit for the same name.
*/
@Test
void secondDeepRefreshLeavesTheGraphIdentical() {
String disktestBefore = dataStructures("DISKTEST");
String umktestBefore = dataStructures("UMKTEST");
deepRefresh();
assertEquals(disktestBefore, dataStructures("DISKTEST"), "DISKTEST's data structures changed on re-ingest");
assertEquals(umktestBefore, dataStructures("UMKTEST"), "UMKTEST's data structures changed on re-ingest");
given().when().get(API + "/search/identifier?name=YPUMPRUE&type=DATA_STRUCTURE")
.then().statusCode(200)
.body("size()", equalTo(1))
.body("[0].sourceFile", not(emptyString()));
given().when().get(API + "/search/identifier?name=KORIN.COMMON&type=DATA_STRUCTURE")
.then().statusCode(200)
.body("size()", equalTo(1));
// The class-name placeholder was renamed, redirected and, having no children, reaped.
given().when().get(API + "/search/identifier?name=COMMON_DA&type=DATA_STRUCTURE")
.then().statusCode(200)
.body("size()", equalTo(0));
}
}

View File

@@ -0,0 +1,179 @@
package com.agenticcode.codeserver.api;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
/**
* KORIN gaps, package A (items 215, 216, 217, 221, 222, 223) end to end on a NatJav project: copycode
* modules and who includes them, transaction boundaries, statement search, the dispatch table of a
* transpiled DECIDE and switch, the online/batch traits, and literal call arguments in search/value.
*/
@QuarkusTest
class NatJavPackageAIT {
private static final String PROJECT = "korin-gaps-a";
@TempDir
static Path root;
private static final String PROGRAM = """
package KORIN.Test;
import innowake.mee.natural.api.*;
/**
* <pre>
* *LCM-HEADER BEGIN ****** $$LCMHEADER:20140410-V00.01**********
* * @FUNCTION Test
* *LCM-HEADER END ****** $$LCMHEADER:20140410-V00.01**********
* *SAG GENERATOR: GRAWE-DIALOG VERSION: 4.5.2.13
* </pre>
*/
@Natural
public class PKGA extends NaturalProgram {
@Override
public void run() {
/* @@INCLUDE:BEGIN(GWSTGESM) */
callnat("GWSTGESC", "4711", xWA_KEY);
/* @@INCLUDE:END(GWSTGESM) */
/* @@INCLUDE:BEGIN(YVABNOUT_member) */
xWA_X = 1;
/* @@INCLUDE:END(YVABNOUT_member) */
int decideCounter1 = 0;
if (EQ(WSOP.OPERATION, "getData")) {
decideCounter1++;
perform("GET_DATA");
} else if (EQ(WSOP.OPERATION, "putData") || EQ(WSOP.OPERATION, "setData")) {
decideCounter1++;
callnat("PUTDATA");
}
switch (WSXXXXOP.OPERATION.getValue()) {
case "updateData":
perform("UPDATE_DATA");
break;
}
if (EQ(DEVICE(), "BATCH")) {
endTransaction();
} else {
inputUsingMap("MAPA", window("FENSTER"));
reinput("Bitte korrigieren");
}
setKey(PF3);
stackData(true, xWA_A);
backoutTransaction();
writeWorkFile(1, xWA_D);
}
public void GET_DATA() {
endTransaction();
}
public void UPDATE_DATA() {
}
@Override
public void onError() {
}
}
""";
@BeforeAll
static void ingest() {
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
write("KORIN/Test/PKGA.java", PROGRAM);
write("KORIN/Test/GWSTGESM.jcopy", "callnat(\"GWSTGESC\", \"4711\", xWA_KEY);\n");
write("KORIN/Test/YVABNOUT_member.jcopy", "xWA_X = 1;\n");
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "natjav", null, null))
.when().post("/api/projects/" + PROJECT).then().statusCode(201);
given().when().post("/api/projects/" + PROJECT + "/refresh?deep=true").then().statusCode(200);
}
private static void write(String relative, String content) {
try {
Path file = root.resolve(relative);
Files.createDirectories(file.getParent());
Files.writeString(file, content);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static String api(String path) {
return "/api/projects/" + PROJECT + path;
}
@Test
void copycodesAreModulesAndTheirCallersAreTheIncluders() {
given().when().get(api("/modules?moduleKind=COPYCODE")).then().statusCode(200)
.body("name", containsInAnyOrder("GWSTGESM", "YVABNOUT_member"));
given().when().get(api("/modules/GWSTGESM/callers")).then().statusCode(200)
.body("items.name", contains("PKGA"))
.body("items[0].edgeKind", equalTo("INCLUDES"));
}
@Test
void transactionsListBothBoundariesWithTheirOwner() {
given().when().get(api("/modules/PKGA/transactions")).then().statusCode(200)
.body("kind", containsInAnyOrder("END_TRANSACTION", "END_TRANSACTION", "BACKOUT_TRANSACTION"))
.body("find { it.function == 'GET_DATA' }.kind", equalTo("END_TRANSACTION"));
}
@Test
void statementSearchFiltersByKindAndCounts() {
given().when().get(api("/search/statement?kind=REINPUT")).then().statusCode(200)
.body("module", contains("PKGA"));
given().when().get(api("/search/statement?kind=set_key,stack_data&countOnly=true")).then().statusCode(200)
.body("count", equalTo(2));
given().when().get(api("/search/statement?kind=ON_ERROR")).then().statusCode(200)
.body("$", hasSize(1));
given().when().get(api("/search/statement?kind=GOTO")).then().statusCode(400)
.body("code", equalTo("KIND_UNSUPPORTED"));
}
@Test
void statementNamesAreNotIdentifiers() {
given().when().get(api("/search/identifier?name=END_TRANSACTION&contains=true")).then().statusCode(200)
.body("name", not(hasItem(startsWith("END_TRANSACTION"))))
.body("type", not(hasItem("STATEMENT")));
}
@Test
void dispatchTableHasTheCallRowsOfDecideAndSwitch() {
given().when().get(api("/modules/PKGA/dispatch-table")).then().statusCode(200)
.body("find { it.target == 'GET_DATA' }.guardValue", equalTo("getData"))
.body("find { it.target == 'GET_DATA' }.guardField", equalTo("WSOP.OPERATION"))
.body("find { it.target == 'GET_DATA' }.edgeKind", equalTo("PERFORM"))
.body("find { it.target == 'PUTDATA' }.guardValues", contains("putData", "setData"))
.body("find { it.target == 'UPDATE_DATA' }.guardField", equalTo("WSXXXXOP.OPERATION"))
.body("find { it.target == 'MAPA' }", nullValue());
}
@Test
void traitsAreInContextAndInTheModuleListing() {
given().when().get(api("/modules/PKGA/context")).then().statusCode(200)
.body("traits.generatorModel", equalTo("GRAWE-DIALOG"))
.body("traits.usesMap", equalTo(true))
.body("traits.usesWorkFile", equalTo(true))
.body("traits.checksDeviceBatch", equalTo(true));
given().when().get(api("/modules?sourceFile=KORIN/Test/PKGA.java")).then().statusCode(200)
.body("[0].generatorModel", equalTo("GRAWE-DIALOG"))
.body("[0].usesMap", equalTo(true));
}
@Test
void literalCallArgumentsAreValues() {
given().when().get(api("/search/value?value=4711")).then().statusCode(200)
.body("find { it.kind == 'CALL_ARGUMENT' }.module", equalTo("PKGA"))
.body("find { it.kind == 'CALL_ARGUMENT' }.name", equalTo("GWSTGESC"));
}
}

View File

@@ -0,0 +1,89 @@
package com.agenticcode.codeserver.api;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import static io.restassured.RestAssured.given;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.*;
/**
* Item 242: {@code ?async=true} answers {@code 202} with a job that {@code GET /refresh-jobs/{id}}
* follows to its end, carrying the same {@code IngestSummary} a synchronous refresh returns. The
* one-refresh-per-project refusal is covered deterministically by {@code RefreshJobsTest}; here a
* tiny project would finish before a second request could race it.
*/
@QuarkusTest
class RefreshJobsIT {
private static final String PROJECT = "refresh-jobs";
@TempDir
static Path root;
@BeforeAll
static void setUp() throws IOException {
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
Files.writeString(root.resolve("JOBMOD.nat"), "DEFINE DATA\nLOCAL\n1 #F (A4)\nEND-DEFINE\n*\nEND\n");
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "natural", null, null))
.when().post("/api/projects/" + PROJECT).then().statusCode(201);
}
private static String status(String id) {
return given().when().get("/api/projects/" + PROJECT + "/refresh-jobs/" + id)
.then().statusCode(200).extract().path("status");
}
@Test
void anAsyncRefreshIsAJobThatEndsWithItsSummary() {
String id = given().when().post("/api/projects/" + PROJECT + "/refresh?deep=true&async=true")
.then().statusCode(202)
.body("project", equalTo(PROJECT))
.body("kind", equalTo("deep refresh"))
.body("status", equalTo("RUNNING"))
.extract().path("id");
await().atMost(Duration.ofMinutes(2)).until(() -> !"RUNNING".equals(status(id)));
given().when().get("/api/projects/" + PROJECT + "/refresh-jobs/" + id).then().statusCode(200)
.body("status", equalTo("DONE"))
.body("progress", nullValue())
.body("finishedAt", notNullValue())
.body("result.examinedFiles", hasItem("JOBMOD.nat"))
.body("result.failed", empty());
given().when().get("/api/projects/" + PROJECT + "/refresh-jobs").then().statusCode(200)
.body("id", hasItem(id));
}
@Test
void aSynchronousRefreshIsListedToo() {
given().when().post("/api/projects/" + PROJECT + "/refresh").then().statusCode(200)
.body("examinedFiles", hasItem("JOBMOD.nat"));
given().when().get("/api/projects/" + PROJECT + "/refresh-jobs").then().statusCode(200)
.body("kind", hasItem("refresh"))
.body("status", everyItem(not(equalTo("FAILED"))));
}
@Test
void anAsyncRecreateIsAJobToo() {
String id = given().when().post("/api/projects/" + PROJECT + "/recreate?async=true")
.then().statusCode(202).body("kind", equalTo("recreate")).extract().path("id");
await().atMost(Duration.ofMinutes(2)).until(() -> !"RUNNING".equals(status(id)));
given().when().get("/api/projects/" + PROJECT + "/refresh-jobs/" + id).then().statusCode(200)
.body("status", equalTo("DONE"));
}
@Test
void anUnknownJobIsAStructured404() {
given().when().get("/api/projects/" + PROJECT + "/refresh-jobs/nope").then().statusCode(404)
.body("code", equalTo("JOB_NOT_FOUND"));
}
}

View File

@@ -60,6 +60,53 @@ class RestEndpointsIT {
}
}
""");
write(pkg, "VerwaltungController.java", """
package com.example.rest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/v1/praemienkonto", produces = "application/json")
public class VerwaltungController {
protected static final String BRANCHE_ANLEGEN_PATH = "/branche-anlegen";
protected static final String KONTONUMMER_PATH = "/buchhaltung/kontoNummern";
protected static final String BUCHHALTUNG_POST_PATH = "/buchhaltung/post";
@PostMapping(path = KONTONUMMER_PATH + BUCHHALTUNG_POST_PATH)
public String kontonummernBuchen(String body) {
return body;
}
@PostMapping(path = KONTONUMMER_PATH + OtherPaths.SUFFIX)
public String unaufloesbar(String body) {
return body;
}
@PostMapping(path = BRANCHE_ANLEGEN_PATH)
public String brancheAnlegen(String body) {
return body;
}
@GetMapping("/vertrag-tree/{typ}/{nummer}")
public String vertragTree(String typ, String nummer) {
return typ;
}
@RequestMapping(value = "/aenderung", method = RequestMethod.PUT)
public String aendern(String body) {
return body;
}
@RequestMapping("/alles")
public String alles() {
return "";
}
}
""");
write(pkg, "PathConstants.java", """
package com.example.rest;
@@ -130,6 +177,35 @@ class RestEndpointsIT {
return given().when().get("/api/projects/" + PROJECT + "/rest-endpoints");
}
/**
* Item 235: Spring MVC handlers are endpoints too — class @RequestMapping + method @Get/PostMapping,
* a path constant, `method = RequestMethod.PUT`, and a verb-less method mapping as ANY.
*/
@Test
void springMvcMappingsAreEndpoints() {
endpoints().then()
.statusCode(200)
.body("find { it.handler == 'brancheAnlegen' }.path", equalTo("/v1/praemienkonto/branche-anlegen"))
.body("find { it.handler == 'brancheAnlegen' }.httpMethod", equalTo("POST"))
.body("find { it.handler == 'vertragTree' }.path", equalTo("/v1/praemienkonto/vertrag-tree/{typ}/{nummer}"))
.body("find { it.handler == 'vertragTree' }.httpMethod", equalTo("GET"))
.body("find { it.handler == 'aendern' }.httpMethod", equalTo("PUT"))
.body("find { it.handler == 'alles' }.httpMethod", equalTo("ANY"));
}
/**
* Item 254: {@code path = A + B} of same-class constants is one path; a part that cannot be resolved
* (another class's constant) makes the handler no endpoint rather than one on the bare class path.
*/
@Test
void aConcatenationOfConstantsIsOnePathAndAnUnresolvablePartIsNoEndpoint() {
endpoints().then()
.statusCode(200)
.body("find { it.handler == 'kontonummernBuchen' }.path",
equalTo("/v1/praemienkonto/buchhaltung/kontoNummern/buchhaltung/post"))
.body("handler", not(hasItem("unaufloesbar")));
}
@Test
void theClassAndMethodPathsAreComposedIntoOnePath() {
endpoints().then()

View File

@@ -2,17 +2,22 @@ package com.agenticcode.codeserver.api;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import jakarta.inject.Inject;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
/**
@@ -20,17 +25,27 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
* A component that imported and called {@code B} is edited to call {@code C}: after the deep refresh
* {@code callees} lists only {@code C}, the old placeholder is gone, and an untouched file keeps every
* edge. The TypeScript half needs the Node sidecar (skipped otherwise); the Java half always runs.
*
* <p>Item 250: the same reap covers {@code CONTAINS} between two nodes that both survive the re-parse —
* the residue the {@code korin} fingerprint found (an {@code IF} nested in an {@code IF} by an older
* parser, both endpoints alive through their positional key, so no node sweep ever took the edge).
*/
@QuarkusTest
class StaleParsedEdgeReapIT {
private static final String TS = "reap-ts";
private static final String JAVA = "reap-java";
private static final String NATURAL = "reap-natural";
@TempDir
static Path tsRoot;
@TempDir
static Path javaRoot;
@TempDir
static Path naturalRoot;
@Inject
Driver driver;
@BeforeAll
static void setUp() {
@@ -110,4 +125,30 @@ class StaleParsedEdgeReapIT {
.body("items.name", not(hasItem("com.example.B")));
callees(JAVA, "com.example.Other").then().statusCode(200).body("items.name", hasItem("com.example.B"));
}
@Test
void aContainsEdgeBetweenTwoSurvivingNodesIsReapedWhenTheNestingChanges() {
// the IF on line 6 sits in subroutine S1; the edit keeps it on line 6 and keeps S1, but moves
// the IF out of the subroutine - both endpoints survive (named / positional key), the edge must not
write(naturalRoot, "NESTMOD.nat", "DEFINE DATA\nLOCAL\n1 #A (N1)\nEND-DEFINE\nDEFINE SUBROUTINE S1\n"
+ "IF #A = 2\n WRITE 'X'\nEND-IF\nEND-SUBROUTINE\nEND\n");
create(NATURAL, naturalRoot, "natural");
refresh(NATURAL);
assertEquals(1L, subroutineContainsIf(), "the first parse puts the IF (line 6) in S1");
write(naturalRoot, "NESTMOD.nat", "DEFINE DATA\nLOCAL\n1 #A (N1)\nEND-DEFINE\nPERFORM S1\n"
+ "IF #A = 2\n WRITE 'X'\nEND-IF\nDEFINE SUBROUTINE S1\nIGNORE\nEND-SUBROUTINE\nEND\n");
refresh(NATURAL);
assertEquals(0L, subroutineContainsIf(), "the re-parse no longer emits S1 CONTAINS IF (line 6)");
}
private long subroutineContainsIf() {
try (Session session = driver.session()) {
return session.run("""
MATCH (a:AstNode {project: $p, type: 'FUNCTION', name: 'S1'})
-[:CONTAINS]->(b:AstNode {project: $p, type: 'CONTROL_FLOW', startLine: 6})
RETURN count(*) AS n""", Map.of("p", NATURAL))
.single().get("n").asLong();
}
}
}

View File

@@ -0,0 +1,108 @@
package com.agenticcode.codeserver.api;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import com.agenticcode.codeserver.service.DeepIngestCoordinator;
import com.agenticcode.neo4jstore.graph.GraphRepository;
import com.agenticcode.neo4jstore.graph.ModuleIngestState;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import jakarta.inject.Inject;
/**
* Item 205: a placeholder module has {@code sourceFile ""} and every fan-out result carries it (the
* call-tree file index, the callers of an unresolved callee). Handed to the fan-out warm it used to be
* resolved against the project root — the root directory itself — and re-ingested on every request,
* because an empty path can never become {@code FULL}. The warm must ignore it: with {@code [""]}
* alone, and with {@code ""} next to an already-FULL file, nothing is warmed and the caller gets
* {@code false} (no re-run).
*/
@QuarkusTest
class WarmEmptyPathIT {
private static final String PROJECT = "item205-warm-empty-path";
private static final String MODULE = "DF_CALLER";
@TempDir
static Path root;
@Inject
DeepIngestCoordinator deepIngestCoordinator;
@Inject
GraphRepository graphRepository;
@BeforeAll
static void ingestDeep() {
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
copyFixture("fixtures/natural/DF_CALLER.nat");
copyFixture("fixtures/natural/DF_CALLEE.nat");
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "natural", null, null))
.when().post("/api/projects/" + PROJECT)
.then().statusCode(201);
given().when().post("/api/projects/" + PROJECT + "/refresh?deep=true")
.then().statusCode(200);
}
private static void copyFixture(String classpathResource) {
String fileName = classpathResource.substring(classpathResource.lastIndexOf('/') + 1);
try (InputStream in = WarmEmptyPathIT.class.getClassLoader().getResourceAsStream(classpathResource)) {
if (in == null) {
throw new IllegalStateException("Resource not found: " + classpathResource);
}
Files.write(root.resolve(fileName), in.readAllBytes());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Test
void anEmptyPathAloneWarmsNothing() {
ModuleIngestState before = state();
assertTrue(before.isFull(), "deep refresh marks the fixture module FULL");
boolean changed = deepIngestCoordinator.ensureDeepMany(PROJECT, List.of("")).await().indefinitely();
assertFalse(changed, "the placeholder path \"\" is not a file and must not trigger a warm");
assertEquals(before.status(), state().status(), "the FULL module is untouched by the empty path");
}
@Test
void anEmptyPathNextToAFullFileWarmsNothing() {
boolean changed = deepIngestCoordinator
.ensureDeepMany(PROJECT, List.of("", MODULE + ".nat")).await().indefinitely();
assertFalse(changed, "a FULL file plus the placeholder path is nothing to warm — no re-run");
}
/**
* Item 224: a NatJav data area is never marked FULL, so warming it never converged and re-ran a
* finalize on every call.
*/
@Test
void aNatJavDataAreaIsNotWarmed() {
boolean changed = deepIngestCoordinator
.ensureDeepMany(PROJECT, List.of("KORIN/Diverse/YPDSGVT_DA.java")).await().indefinitely();
assertFalse(changed, "a data area has nothing a deep ingest adds");
}
private ModuleIngestState state() {
return graphRepository.moduleIngestState(PROJECT, MODULE).await().indefinitely();
}
}

View File

@@ -0,0 +1,155 @@
package com.agenticcode.codeserver.service;
import com.agenticcode.parsercore.ast.model.NodeType;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static java.util.Objects.requireNonNull;
import static org.junit.jupiter.api.Assertions.*;
/**
* The two ways two NatJav files end up claiming one module name, and the rule that tells them apart.
* The fixtures reproduce the reference corpus's real cases by name.
*/
class NatJavIdentitiesTest {
@TempDir
Path root;
/**
* Writes the head of a generated NatJav file — the annotation and the class declaration are all
* the scan reads.
*/
private Path file(String relativePath, @Nullable String moduleName, String baseClass) throws IOException {
Path path = root.resolve(relativePath);
Path parent = requireNonNull(path.getParent());
Files.createDirectories(parent);
String className = path.getFileName().toString().replace(".java", "");
String annotation = moduleName == null ? "@Natural" : "@Natural(moduleName=\"" + moduleName + "\")";
Files.writeString(path, "package " + parent.getFileName() + ";\n\n"
+ "import innowake.mee.natural.api.*;\n\n"
+ annotation + "\npublic class " + className + " extends " + baseClass + " {\n}\n");
return path;
}
private static NatJavIdentities.Rename require(Map<String, NatJavIdentities.Rename> renames, String key) {
NatJavIdentities.Rename rename = renames.get(key);
assertNotNull(rename, "expected a rename for " + key + ", got " + renames.keySet());
return rename;
}
private Map<String, NatJavIdentities.Rename> scan(List<Path> files) {
List<ProjectIngestService.Candidate> candidates = new ArrayList<>();
for (Path path : files) {
NodeType kind = path.getFileName().toString().endsWith("_DA.java")
? NodeType.DATA_STRUCTURE : NodeType.MODULE;
candidates.add(new ProjectIngestService.Candidate(path,
new SourceFiles.Kind(SourceFiles.Language.NATJAV, kind)));
}
return NatJavIdentities.scan(root, candidates);
}
/**
* Natural module names are unique per library, not globally: {@code KORIN} and {@code ABFRAGE}
* may each hold an {@code ACOMMON}. Both are real, so both are kept — qualified by library.
*/
@Test
void libraryHomonymsAreQualifiedByLibrary() throws IOException {
Path korin = file("KORIN/ACOMMON.java", null, "NaturalProgram");
Path abfrage = file("ABFRAGE/ACOMMON.java", null, "NaturalProgram");
Map<String, NatJavIdentities.Rename> renames = scan(List.of(korin, abfrage));
assertEquals("KORIN.ACOMMON", require(renames, "KORIN/ACOMMON.java").name());
assertEquals("ABFRAGE.ACOMMON", require(renames, "ABFRAGE/ACOMMON.java").name());
assertNull(require(renames, "KORIN/ACOMMON.java").conflictingName(),
"a library qualification is not a correction — nothing was in dispute");
}
/**
* The GDA {@code COMMON} exists in both libraries as {@code COMMON_DA.java}; the qualification
* uses the Natural name, not the class name.
*/
@Test
void dataAreaHomonymsAreQualifiedToo() throws IOException {
Path korin = file("KORIN/COMMON_DA.java", "COMMON", "NaturalData");
Path abfrage = file("ABFRAGE/COMMON_DA.java", "COMMON", "NaturalData");
Map<String, NatJavIdentities.Rename> renames = scan(List.of(korin, abfrage));
assertEquals("KORIN.COMMON", require(renames, "KORIN/COMMON_DA.java").name());
assertEquals("ABFRAGE.COMMON", require(renames, "ABFRAGE/COMMON_DA.java").name());
}
/**
* {@code ZB80BM6.java} declaring {@code moduleName="ZB80BM5"} is a copied header: {@code
* ZB80BM5.java} exists and owns the name. The impostor is ingested under its own name, and the
* declaration it lost is recorded rather than dropped.
*/
@Test
void copiedHeaderLosesToTheFileThatOwnsTheName() throws IOException {
Path owner = file("KORIN/Praemienkonto/ZB80BM5.java", null, "NaturalProgram");
Path impostor = file("KORIN/Praemienkonto/ZB80BM6.java", "ZB80BM5", "NaturalProgram");
Map<String, NatJavIdentities.Rename> renames = scan(List.of(owner, impostor));
assertNull(renames.get("KORIN/Praemienkonto/ZB80BM5.java"), "the owner keeps its name");
NatJavIdentities.Rename rename = require(renames, "KORIN/Praemienkonto/ZB80BM6.java");
assertEquals("ZB80BM6", rename.name());
assertEquals("ZB80BM5", rename.conflictingName());
}
/**
* The same defect in a data area: {@code WSK386P_DA.java} declares {@code WSK385P}, which
* {@code WSK385P_DA.java} owns. The suffix the transpiler appends is not part of the name.
*/
@Test
void copiedHeaderInADataAreaFallsBackToTheClassNameWithoutTheSuffix() throws IOException {
Path owner = file("KORIN/Webservices/WSK385P_DA.java", "WSK385P", "NaturalData");
Path impostor = file("KORIN/Webservices/WSK386P_DA.java", "WSK385P", "NaturalData");
Map<String, NatJavIdentities.Rename> renames = scan(List.of(owner, impostor));
assertNull(renames.get("KORIN/Webservices/WSK385P_DA.java"));
assertEquals("WSK386P", require(renames, "KORIN/Webservices/WSK386P_DA.java").name());
}
@Test
void anUncontestedNameIsNeverRenamed() throws IOException {
Path a = file("KORIN/V_MIT04.java", "V-MIT04", "NaturalSubprogram");
Path b = file("KORIN/XABBRUCH.java", "#ABBRUCH", "NaturalProgram");
assertTrue(scan(List.of(a, b)).isEmpty(), "nothing collides, so nothing is renamed");
}
/**
* Two files in the <em>same</em> library with no owner between them are genuinely
* indistinguishable. Renaming one would invent a distinction, so both are left for the duplicate
* detection to report.
*/
@Test
void indistinguishableClaimsAreLeftToDuplicateDetection() throws IOException {
Path a = file("KORIN/AAA.java", "SHARED", "NaturalProgram");
Path b = file("KORIN/BBB.java", "SHARED", "NaturalProgram");
assertTrue(scan(List.of(a, b)).isEmpty());
}
@Test
void nonNatJavCandidatesAreIgnored() throws IOException {
Path path = file("KORIN/ACOMMON.java", null, "NaturalProgram");
List<ProjectIngestService.Candidate> javaCandidates = List.of(
new ProjectIngestService.Candidate(path,
new SourceFiles.Kind(SourceFiles.Language.JAVA, NodeType.MODULE)));
assertTrue(NatJavIdentities.scan(root, javaCandidates).isEmpty());
}
}

View File

@@ -0,0 +1,95 @@
package com.agenticcode.codeserver.service;
import com.agenticcode.neo4jstore.graph.IngestProgress;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.*;
/**
* Item 242: one refresh per project at a time, asynchronous jobs report their phase and end state.
*/
class RefreshJobsTest {
private static final IngestSummary SUMMARY = new IngestSummary(3, List.of(), List.of(), List.of(), List.of(), null);
private final IngestProgress progress = new IngestProgress();
private final RefreshJobs jobs = new RefreshJobs(progress);
@Test
void aSecondRefreshOfARunningProjectIsRefusedWithTheRunningJobAndItsPhase() throws Exception {
CountDownLatch release = new CountDownLatch(1);
RefreshJobs.RefreshJob started = jobs.start("p", "deep refresh", () -> {
progress.report("p", "finalize full [3/45] link-calls");
awaitLatch(release);
return SUMMARY;
});
assertEquals(RefreshJobs.Status.RUNNING, started.status());
await().atMost(Duration.ofSeconds(10)).until(() -> "finalize full [3/45] link-calls".equals(
job("p", started.id()).progress()));
RefreshJobs.RefreshRunningException refused = assertThrows(RefreshJobs.RefreshRunningException.class,
() -> jobs.runNow("p", "refresh", () -> SUMMARY));
assertEquals(started.id(), refused.running().id());
assertEquals("finalize full [3/45] link-calls", refused.running().progress());
assertThrows(RefreshJobs.RefreshRunningException.class, () -> jobs.start("p", "recreate", () -> SUMMARY));
// another project is not blocked
assertSame(SUMMARY, jobs.runNow("q", "refresh", () -> SUMMARY));
release.countDown();
await().atMost(Duration.ofSeconds(10)).until(() -> job("p", started.id()).status() == RefreshJobs.Status.DONE);
RefreshJobs.RefreshJob done = job("p", started.id());
assertSame(SUMMARY, done.result());
assertNull(done.progress());
assertNotNull(done.finishedAt());
// the slot is free again
assertSame(SUMMARY, jobs.runNow("p", "refresh", () -> SUMMARY));
assertEquals(2, jobs.list("p").size(), "a refused refresh is not a job");
assertNull(jobs.get("q", started.id()), "a job is only found under its own project");
}
@Test
void aFailedRunIsReportedAndFreesTheProject() throws IOException {
assertThrows(IOException.class, () -> jobs.runNow("p", "refresh", () -> {
throw new IOException("root gone");
}));
RefreshJobs.RefreshJob failed = jobs.list("p").getFirst();
assertEquals(RefreshJobs.Status.FAILED, failed.status());
assertTrue(failed.error() != null && failed.error().contains("root gone"));
RefreshJobs.RefreshJob async = jobs.start("p", "refresh", () -> {
throw new IllegalStateException("boom");
});
await().atMost(Duration.ofSeconds(10)).until(() -> job("p", async.id()).status() == RefreshJobs.Status.FAILED);
assertSame(SUMMARY, jobs.runNow("p", "refresh", () -> SUMMARY));
}
@Test
void onlyTheLastJobsAreKept() throws IOException {
for (int i = 0; i < RefreshJobs.KEEP + 5; i++) {
jobs.runNow("p", "refresh", () -> SUMMARY);
}
assertEquals(RefreshJobs.KEEP, jobs.list("p").size());
}
private RefreshJobs.RefreshJob job(String project, String id) {
RefreshJobs.RefreshJob job = jobs.get(project, id);
assertNotNull(job, "job " + id + " not found");
return java.util.Objects.requireNonNull(job);
}
private static void awaitLatch(CountDownLatch latch) {
try {
assertTrue(latch.await(30, TimeUnit.SECONDS));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
}
}
}

View File

@@ -4,6 +4,7 @@ import com.agenticcode.parsercore.ast.model.NodeType;
import org.junit.jupiter.api.Test;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -13,7 +14,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Unit tests for {@link SourceFiles#classify} — the extension → (language, node kind) mapping that
* gates which files the ingester picks up. Pins the Natural data-area extensions incl. {@code .gda}
* (roadmap item 46c) and that unknown extensions are not ingestible.
* (roadmap item 46c), that unknown extensions are not ingestible, and that a {@code .java} file is
* read as NatJav only when its project declares that language.
*/
class SourceFilesTest {
@@ -40,6 +42,45 @@ class SourceFilesTest {
}
}
/**
* NatJav is the one case the extension cannot decide: its files are {@code .java}. Without a
* project language they must stay plain Java, or every Java project would change meaning.
*/
@Test
void javaStaysJavaWithoutANatJavProject() {
assertEquals(new SourceFiles.Kind(SourceFiles.Language.JAVA, NodeType.MODULE),
SourceFiles.classify(Path.of("KORIN/ACOMMON.java"), null));
assertEquals(new SourceFiles.Kind(SourceFiles.Language.JAVA, NodeType.MODULE),
SourceFiles.classify(Path.of("KORIN/ACOMMON.java"), "java"));
}
@Test
void natJavProjectReadsJavaFilesAsNatJav() {
assertEquals(new SourceFiles.Kind(SourceFiles.Language.NATJAV, NodeType.MODULE),
SourceFiles.classify(Path.of("KORIN/ACOMMON.java"), "natjav"));
assertEquals(new SourceFiles.Kind(SourceFiles.Language.NATJAV, NodeType.MODULE),
SourceFiles.classify(Path.of("KORIN/ACOMMON.java"), "NatJav"), "language match is case-insensitive");
}
/**
* The {@code _DA} suffix is to NatJav what {@code .lda}/{@code .pda}/{@code .gda} is to Natural:
* the only file-level signal that this is a data area rather than a program.
*/
@Test
void natJavDataAreasAreDataStructures() {
assertEquals(new SourceFiles.Kind(SourceFiles.Language.NATJAV, NodeType.DATA_STRUCTURE),
SourceFiles.classify(Path.of("KORIN/COMMON_DA.java"), "natjav"));
assertEquals(new SourceFiles.Kind(SourceFiles.Language.NATJAV, NodeType.MODULE),
SourceFiles.classify(Path.of("SYSTEM/BANKEZ_DDM.java"), "natjav"),
"a DDM is a module, not a data area");
}
@Test
void naturalExtensionsAreUnaffectedByTheProjectLanguage() {
assertEquals(new SourceFiles.Kind(SourceFiles.Language.NATURAL, NodeType.MODULE),
SourceFiles.classify(Path.of("WSUBPX0S.nat"), "natjav"));
}
@Test
void typeScriptAndCssAreModulesButDeclarationFilesAreNot() {
// item 192
@@ -72,4 +113,19 @@ class SourceFilesTest {
assertNull(SourceFiles.classify(Path.of("SCREEN.map")));
assertNull(SourceFiles.classify(Path.of("notes.txt")));
}
@Test
void aBareExcludeNameMatchesAnyComponentAnAnchoredOneOnlyTheRootDirectory() {
Path root = Path.of("/k3");
Path frontEnd = root.resolve("Koala/K4/src/app/x.ts");
Path backend = root.resolve("K3_Koala/src/main/java/at/k3/koala/v1/X.java");
assertTrue(SourceFiles.isExcluded(root, frontEnd, List.of("Koala")));
assertTrue(SourceFiles.isExcluded(root, backend, List.of("Koala")));
assertTrue(SourceFiles.isExcluded(root, frontEnd, List.of("/Koala")));
assertTrue(SourceFiles.isExcluded(root, frontEnd, List.of("/koala/")));
assertFalse(SourceFiles.isExcluded(root, backend, List.of("/Koala")));
assertFalse(SourceFiles.isExcluded(root, root.resolve("KoalaX/a.ts"), List.of("/Koala")));
assertTrue(SourceFiles.isExcluded(root, root.resolve("Koala/K4/a.ts"), List.of("/Koala/K4")));
assertFalse(SourceFiles.isExcluded(root, root.resolve("Koala/k4-suche/a.ts"), List.of("/Koala/K4")));
}
}

View File

@@ -0,0 +1,7 @@
package fixtures.extendsfilter;
/**
* A subclass of a subclass (item 209 fixture): below {@link AbstractProcessingStep}, but not directly.
*/
public class PremiumAccountProcessingStep extends AccountProcessingStep {
}

View File

@@ -0,0 +1,652 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN;
import innowake.mee.natural.api.*;
@Natural(moduleName="COMMON")
public class COMMON_DA extends NaturalData {
@Global
/*
* GDA für LIBRARY KORIN INCLUSIVE
* REDEFINITIONEN
*/
@L1 A gPROGRAMM = A(8 ,"+PROGRAMM");
@L1 A gBPOL_PARM = A(17 ,"+BPOL-PARM");
@L1 N gBBST_POLNR = N(9,0 ,"+BBST-POLNR");
@L1 A gBBST_ART = A(9 ,"+BBST-ART");
@L1 A gABTEILUNG = A(5 ,"+ABTEILUNG");
@L1 A gPASSWORD = A(8 ,"+PASSWORD");
@L1 A gGETMAIL = A(1 ,"+GETMAIL");
@L1 A gERROR_PROGRAM = A(8 ,"+ERROR-PROGRAM");
@L1 A gERROR_STAT = A(1 ,"+ERROR-STAT");
@L1 N gERROR_LINE = N(4,0 ,"+ERROR-LINE");
@L1 N gERROR_NR = N(4,0 ,"+ERROR-NR");
@L1 A gERGEBNIS = A(79 ,"+ERGEBNIS");
@L1 A gSPOOL_PARM = A(80 ,"+SPOOL-PARM");
@L1 Redefine gSPOOL_PARM;
@L2 A xSPUSER = A(8 ,"#SPUSER");
@L2 A xSPDEST = A(8 ,"#SPDEST");
@L2 A xSPFORM = A(1 ,"#SPFORM");
@L1 A gINIT_VAR = A(240 ,"+INIT-VAR");
@L1 Redefine gINIT_VAR; // REDEF. BEGIN : +INIT-VAR
@L2 A xRA_INITVAR_GETMAIL = A(1 ,"#RA-INITVAR-GETMAIL");// GETMAIL duchgeführt Y/N
@L2 N xINIT_DATIME = N(15,0 ,"#INIT-DATIME");// STARTUP Datum/Zeitder
@L2 Redefine xINIT_DATIME; // letzten Startup-Durchführung
@L3 N xINIT_DATE = N(8,0 ,"#INIT-DATE");// "YYYYMMDDHHMMSSZ"
@L3 Redefine xINIT_DATE;
@L4 A xINIT_DATE_ALPHA = A(8 ,"#INIT-DATE-ALPHA");
@L3 N xINIT_TIME = N(7,0 ,"#INIT-TIME");
@L3 Redefine xINIT_TIME;
@L4 A xINIT_TIME_ALPHA = A(7 ,"#INIT-TIME-ALPHA");
@L2 A xINIT_USER = A(8 ,"#INIT-USER"); // STARTUP U-ID d. Current LOGON
@L2 A xINIT_ID = A(8 ,"#INIT-ID"); // STARTUP Terminal-ID
@L2 A xINIT_GROUP = A(8 ,"#INIT-GROUP"); // STARTUP GROUP-ID, über die
/* /* gelinkt */
@L2 A xSTARTUP_PROGRAM = A(8 ,"#STARTUP-PROGRAM");// STARTUP Startup-Programm
@L2 A xRESTART_PROGRAM = A(8 ,"#RESTART-PROGRAM");// STARTUP Restart-Programm
@L2 A xERROR_PROGRAM = A(8 ,"#ERROR-PROGRAM");// #ERR letztes fehlerhaftes PRG
/* /* STARTUP Error-Programm */
@L2 A xRA_INITVAR_CICS_NAME = A(8 ,"#RA-INITVAR-CICS-NAME");// STARTUP und APPLID
@L2 Redefine xRA_INITVAR_CICS_NAME; // "CSWVCICS"/"CSWVCICT"
@L3 A xCICSNAME = A(8 ,"#CICSNAME"); // Redef. aus Namensgründen
@L2 Redefine xRA_INITVAR_CICS_NAME;
@L3 A xCICS_NAME = A(8 ,"#CICS-NAME"); // Redef. aus Namensgründen
@L2 A FILLER = A(8 ,"FILLER");
@L2 A xUSER_NAME = A(32 ,"#USER-NAME"); // STARTUP User-Name
@L2 A xAPPL_NAME = A(32 ,"#APPL-NAME"); // STARTUP Anwendungs-Name
@L2 A xSYS_SCHALTER = A(8 ,"#SYS-SCHALTER");// DIV SYSTEM-Schalter
@L2 Redefine xSYS_SCHALTER;
@L3 A xSYS_RESTART = A(1 ,"#SYS-RESTART");// #ERR bei Absturz Restart Y/N
@L3 A xSYS_CANCEL = A(1 ,"#SYS-CANCEL");// #CAN BATCH CANCELNin #ERROR
@L3 A xERROR_STAT = A(1 ,"#ERROR-STAT");// #ERR letzter Fehler-Status
@L3 A xERROR_OPTN = A(1 ,"#ERROR-OPTN");// #ERR RETRY ON ERRORS Y/N
@L3 A xERROR_SAME = A(1 ,"#ERROR-SAME");// #ERR same ERROR wie zuvor Y/N
/* 0=NEIN 1=PROG 2=NUMMER 3=BEIDE */
@L3 A xERROR_LOG = A(1 ,"#ERROR-LOG"); // #ERR N=kein ERROR-Logging
@L3 A xMONITOR = A(1 ,"#MONITOR"); // STARTUP B=BATCH, C=CICS
/* /* P=COMPLETE */
@L3 A FILLER2 = A(1 ,"FILLER2");
@L2 A xERROR_PROG = A(8 ,"#ERROR-PROG");
@L2 N xERROR_NR = N(4,0 ,"#ERROR-NR"); // #ERR letzte Fehlernummer
@L2 Redefine xERROR_NR;
@L3 A xERRAX = A(4 ,"#ERRAX");
@L2 N xERROR_LINE = N(4,0 ,"#ERROR-LINE"); // #ERR letzte fehlerhafte Zeile
@L2 A xMESSAGE = A(72 ,"#MESSAGE"); // #ERR Fehlermeldung
@L2 Redefine xMESSAGE;
@L3 A xMANF = A(1 ,"#MANF");
@L3 A STAR = A(1 ,"STAR");
@L3 A RESTMSG = A(70 ,"RESTMSG");
@L1 A gBERECHTIGUNGEN = A("+BERECHTIGUNGEN");
@L1 A gVSU = A(250 ,"+VSU");
@L1 A gKIS = A(250 ,"+KIS");
@L1 Redefine gKIS; // REDEF. BEGIN : +KIS
@L2 N xRN_KIS_ZAHL = N(3,0 ,"#RN-KIS-ZAHL");
@L2 B xRA_KIS_KUISNT = B(3 ,"#RA-KIS-KUISNT").defineArray(range(1, 20));
@L2 Redefine xRA_KIS_KUISNT; // REDEF. BEGIN : #RA-KIS-KUISNT
@L3 B xRA_KIS_KUISN = B(3 ,"#RA-KIS-KUISN");
@L2 N xRN_KIS_OVZL = N(3,0 ,"#RN-KIS-OVZL");
@L2 B xRA_KIS_OVISN = B(3 ,"#RA-KIS-OVISN").defineArray(range(1, 60));
@L1 A gUMS = A(250 ,"+UMS");
@L1 Redefine gUMS; // REDEF. BEGIN : +UMS
@L2 A xRA_UMS_CODE = A(8 ,"#RA-UMS-CODE");
@L2 P xRN_UMS_LAST_VR = P(3,0 ,"#RN-UMS-LAST-VR");
@L2 A xRA_UMS_MD_KZ1_BKLZ_KZ = A(1 ,"#RA-UMS-MD-KZ1-BKLZ-KZ");
@L2 A xRA_UMS_MD_KZ1_TARIF = A(1 ,"#RA-UMS-MD-KZ1-TARIF");
@L2 A xRA_UMS_WAEHRUNG_MD = A(3 ,"#RA-UMS-WAEHRUNG-MD");
@L2 N xRN_UMS_LAST_LOAD = N(6,0 ,"#RN-UMS-LAST-LOAD");
@L2 A xUMS_VORG_OPID = A(3 ,"#UMS-VORG-OPID");
@L2 A xUMS_FORCE = A(1 ,"#UMS-FORCE"); // Beharrung auf eigene
/* * /* PF-Tasten-Belegung */
@L2 N xUMS_SNR_FIL = N(2,0 ,"#UMS-SNR-FIL");// Schaden-Filial-Nummer
@L2 A xUMS_SNR_KREIS = A(2 ,"#UMS-SNR-KREIS");// Schaden-Kreis zB.:'SA'
@L2 N xUMS_SNR_JAHR = N(4,0 ,"#UMS-SNR-JAHR");// Schaden-Jahr
@L2 N xUMS_SNR_LFD_NR = N(6,0 ,"#UMS-SNR-LFD-NR");// Schaden-Laufende-Nummer
@L2 A xUMS_SNR_STATUS = A(1 ,"#UMS-SNR-STATUS");// Status-Byte 'O','S','H'
@L2 A xUMS_ART = A(2 ,"#UMS-ART"); // Schadenart
@L2 A xUMS_REFERENT = A(8 ,"#UMS-REFERENT");// Referent
@L2 A xUMS_EVI_TYP = A(1 ,"#UMS-EVI-TYP"); // eigen oder fremd-Vi
@L2 P xUMS_IDENTCOUNTER = P(5,0 ,"#UMS-IDENTCOUNTER");
@L2 P xUMS_OUTPUTNR = P(5,0 ,"#UMS-OUTPUTNR");
@L2 P xUMS_EREIGNIS_DT = P(8,0 ,"#UMS-EREIGNIS-DT");// nur für LV (= Stornodatum)
@L2 P xUMS_SCN_HIST = P(3,0 ,"#UMS-SCN-HIST");// merken Hist-Lesestand
@L2 B xUMS_ANZEIGEDAT_KPL = B(3 ,"#UMS-ANZEIGEDAT-KPL");// für Aufruf SK10
@L2 N xUMS_SKT_FIL = N(2,0 ,"#UMS-SKT-FIL");// Filial-Nummer Kontostamm
@L2 A xUMS_SKT_KREIS = A(2 ,"#UMS-SKT-KREIS");// Schaden-Kreis Kontostamm
@L2 N xUMS_SKT_JAHR = N(4,0 ,"#UMS-SKT-JAHR");// Schaden-Jahr Kontostamm
@L2 N xUMS_SKT_LFD_NR = N(6,0 ,"#UMS-SKT-LFD-NR");// Schaden-Lfd-Nr Kontostamm
@L2 N xUMS_SKT_BRAN = N(2,0 ,"#UMS-SKT-BRAN");// Schaden-Branche Kontostamm
@L2 A xUMS_WORKSTATION_ID = A(8 ,"#UMS-WORKSTATION-ID");// Workstation (OD) *** frei ***
@L2 A xRA_UMS_BRGR = A(1 ,"#RA-UMS-BRGR").defineArray(range(1,3));
@L2 A xUMS_WF_ABT = A(8 ,"#UMS-WF-ABT"); // Workflow-Abteilung(für OD)
@L2 A xUMS_WF_STAT = A(8 ,"#UMS-WF-STAT"); // Workflow-Station(für OD)
@L2 A xUMS_WF_OPID = A(8 ,"#UMS-WF-OPID"); // Workflow-Opid(für OD)
@L2 P xUMS_WF_SORT_DOKU_ART = P(3,0 ,"#UMS-WF-SORT-DOKU-ART").defineArray(range(1,5));// WF-Dokument(für OD)
@L2 P xUMS_KEYCOUNTER = P(5,0 ,"#UMS-KEYCOUNTER").defineArray(range(1,10));// Identcounter aus File111
@L2 A xUMS_SKT_BELEGTYP = A(1 ,"#UMS-SKT-BELEGTYP");// Schaden-Konto-Belegtyp
@L2 P xUMS_SKT_POS_IDC = P(5,0 ,"#UMS-SKT-POS-IDC");// Identcounter Pos.Buchung-SKT
@L2 P xUMS_SKT_BRAN_IDC = P(5,0 ,"#UMS-SKT-BRAN-IDC");// Identcounter Bran.Buchung-SKT
@L2 P xUMS_SKT_ABST_IDC = P(5,0 ,"#UMS-SKT-ABST-IDC");// Identcounter Abst.Buchung-SKT
@L2 A xUMS_TEXT_FN95 = A(1 ,"#UMS-TEXT-FN95");// Text-Option im FN95
@L2 A xUMS_REGRESS_RATE = A(2 ,"#UMS-REGRESS-RATE");// Schaden-Regreß oder -Rate
@L2 P xUMS_REGRESS_NR = P(6,0 ,"#UMS-REGRESS-NR");// Schaden-Regreß-Nr.
@L2 A xUMS_DAT2_FN95 = A(1 ,"#UMS-DAT2-FN95");// 2. Datumszeile FN95 optional
@L2 N xUMS_PC_DAT = N(8,0 ,"#UMS-PC-DAT"); // Pc-Antragsdatum
@L2 N xUMS_PC_NUM = N(4,0 ,"#UMS-PC-NUM"); // Pc-Antragsnummer
@L2 A xUMS_DRUCKER_ID = A(10 ,"#UMS-DRUCKER-ID");// Drucker-Id (OD)
@L2 A xUMS_KEY_STAT_DI32 = A(8 ,"#UMS-KEY-STAT-DI32");// Station für AufrufDI32 (OD)
@L2 B xUMS_SKT_ANZEIGEDAT_KPL = B(3 ,"#UMS-SKT-ANZEIGEDAT-KPL");// Anzeigedatum-KPL für Sammelb.
@L2 P xUMS_WINDOW_NUM = P(2,0 ,"#UMS-WINDOW-NUM");// Image-Display (OD)
@L2 P xUMS_STORNO_GRUND = P(2,0 ,"#UMS-STORNO-GRUND");// stornogrund (nur LV)
@L2 A xUMS_DOI_KEY_ART = A(2 ,"#UMS-DOI-KEY-ART");// Kz f. Einschränkung Di30 (OD)
@L2 P xUMS_SKT_WERTEDATUM = P(8,0 ,"#UMS-SKT-WERTEDATUM");// Schaden-Konto-Wertedatum
@L2 P xUMS_SKT_BANKSCHLUESSEL = P(2,0 ,"#UMS-SKT-BANKSCHLUESSEL");// Schaden-Konto-Bankschlüssel
@L2 A xUMS_REST = A(28 ,"#UMS-REST"); // Bytes frei
@L1 A gPEL = A(250 ,"+PEL");
@L1 Redefine gPEL; // REDEF. BEGIN : +PEL
@L2 A xRA_PEL_ABT = A(1 ,"#RA-PEL-ABT");
@L2 N xRN_PEL_VPOL = N(8,0 ,"#RN-PEL-VPOL");
@L2 N xRN_PEL_KPNR = N(8,0 ,"#RN-PEL-KPNR");
@L2 N xRN_PEL_GT = N(8,0 ,"#RN-PEL-GT");
@L2 N xRN_PEL_BR1 = N(8,0 ,"#RN-PEL-BR1");
@L2 N xRN_PEL_BR2 = N(8,0 ,"#RN-PEL-BR2");
@L2 N xRN_PEL_BUCH = N(8,0 ,"#RN-PEL-BUCH");
@L2 N xRN_PEL_EINL = N(6,0 ,"#RN-PEL-EINL");
@L2 A xRA_PEL_FREI = A(2 ,"#RA-PEL-FREI");
@L2 N xRN_PEL_TEXT = N(8,0 ,"#RN-PEL-TEXT");
@L2 N xRN_PEL_STAT = N(8,0 ,"#RN-PEL-STAT");
@L2 N xRN_PEL_AGT = N(8,0 ,"#RN-PEL-AGT");
@L2 N xRN_PEL_ABR1 = N(8,0 ,"#RN-PEL-ABR1");
@L2 N xRN_PEL_ABR2 = N(8,0 ,"#RN-PEL-ABR2");
@L2 N xRN_PEL_ATXT = N(8,0 ,"#RN-PEL-ATXT");
@L2 N xRN_PEL_INDX = N(2,0 ,"#RN-PEL-INDX");
@L2 N xRN_PEL_VDAT = N(6,0 ,"#RN-PEL-VDAT");
@L2 A xRA_PEL_EFKZ = A(1 ,"#RA-PEL-EFKZ");
@L2 A xRA_PEL_DRUC = A(4 ,"#RA-PEL-DRUC");
@L2 A xRA_PEL_VART = A(3 ,"#RA-PEL-VART");
@L2 N xRN_PEL_KREI = N(3,0 ,"#RN-PEL-KREI");
@L2 A xRA_PEL_VAE = A(3 ,"#RA-PEL-VAE");
@L2 A xRA_PEL_BNEU = A(3 ,"#RA-PEL-BNEU");
@L2 A xRA_PEL_FILL118 = A(118 ,"#RA-PEL-FILL118");
@L2 A xRA_PEL_MENU = A(2 ,"#RA-PEL-MENU");
@L1 Redefine gPEL; // REDEF. BEGIN : +PEL
@L2 N xRN_KFZ_MENR = N(1,0 ,"#RN-KFZ-MENR");
@L2 N xRN_KFZ_IEVI = N(3,0 ,"#RN-KFZ-IEVI");
@L2 N xRN_KFZ_IVV = N(3,0 ,"#RN-KFZ-IVV");
@L2 N xRN_KFZ_HIST = N(3,0 ,"#RN-KFZ-HIST");
@L2 N xRN_KFZ_STND = N(3,0 ,"#RN-KFZ-STND");
@L2 A xRA_KFZ_EVBR = A(30 ,"#RA-KFZ-EVBR");
@L2 Redefine xRA_KFZ_EVBR;
@L3 A xRA_KFZ_BRA = A(3 ,"#RA-KFZ-BRA").defineArray(range(1, 10));
@L2 Redefine xRA_KFZ_EVBR;
@L3 N xRN_KFZ_BRA1 = N(3,0 ,"#RN-KFZ-BRA1");
@L3 N xRN_KFZ_BRA2 = N(3,0 ,"#RN-KFZ-BRA2");
@L3 N xRN_KFZ_BRA3 = N(3,0 ,"#RN-KFZ-BRA3");
@L3 N xRN_KFZ_BRA4 = N(3,0 ,"#RN-KFZ-BRA4");
@L3 N xRN_KFZ_BRA5 = N(3,0 ,"#RN-KFZ-BRA5");
@L2 N xRN_KFZ_VDAT = N(8,0 ,"#RN-KFZ-VDAT");
@L2 A xRA_KFZ_AREV = A(1 ,"#RA-KFZ-AREV");
@L2 A xRA_KFZ_SABE = A(3 ,"#RA-KFZ-SABE");
@L2 A xRA_KFZ_RAG = A(1 ,"#RA-KFZ-RAG");
@L2 P xRN_KFZ_OZW = P(3,0 ,"#RN-KFZ-OZW");
@L2 N xRN_KFZ_VVAE = N(8,0 ,"#RN-KFZ-VVAE");
@L2 A xRA_KFZ_VEF = A(1 ,"#RA-KFZ-VEF");
@L2 A xRA_KFZ_VVAR = A(3 ,"#RA-KFZ-VVAR");
@L2 N xRN_KFZ_VEGD = N(8,0 ,"#RN-KFZ-VEGD");
@L2 N xRN_KFZ_VELD = N(8,0 ,"#RN-KFZ-VELD");
@L2 A xRA_KFZ_VDRU = A(4 ,"#RA-KFZ-VDRU");
@L2 A xRA_KFZ_PZSG = A(3 ,"#RA-KFZ-PZSG");
@L2 N xRN_KFZ_LESE = N(3,0 ,"#RN-KFZ-LESE");
@L2 N xRN_KFZ_FZW = N(2,0 ,"#RN-KFZ-FZW");
@L2 N xRN_KFZ_VI10 = N(2,0 ,"#RN-KFZ-VI10");
@L2 N xRN_KFZ_VD = N(8,0 ,"#RN-KFZ-VD");
@L2 N xRN_KFZ_VB = N(8,0 ,"#RN-KFZ-VB");
@L2 A xRA_KFZ_KFKZ = A(11 ,"#RA-KFZ-KFKZ");
@L2 N xRN_KFZ_AWDT = N(8,0 ,"#RN-KFZ-AWDT");
@L2 A xRA_KFZ_AWKZ = A(1 ,"#RA-KFZ-AWKZ");
@L2 N xRN_KFZ_AEND = N(8,0 ,"#RN-KFZ-AEND");
@L2 A xRA_KFZ_VABT = A(3 ,"#RA-KFZ-VABT");
@L2 A xRA_KFZ_OPID = A(3 ,"#RA-KFZ-OPID");
@L2 N xRN_KFZ_DATV = N(8,0 ,"#RN-KFZ-DATV");
@L2 N xRN_KFZ_DATB = N(8,0 ,"#RN-KFZ-DATB");
@L2 A xRA_KFZ_FIL = A(2 ,"#RA-KFZ-FIL");
@L2 N xRN_KFZ_KRBR = N(4,0 ,"#RN-KFZ-KRBR");
@L2 Redefine xRN_KFZ_KRBR; // REDEF. BEGIN : #RN-KFZ-KRBR
@L3 N xRN_KFZ_KR = N(2,0 ,"#RN-KFZ-KR");
@L3 N xRN_KFZ_BR = N(2,0 ,"#RN-KFZ-BR");
@L2 A xRA_KFZ_VAE1 = A(3 ,"#RA-KFZ-VAE1");
@L2 A xRA_KFZ_VAE2 = A(3 ,"#RA-KFZ-VAE2");
@L2 A xRA_KFZ_SUMM = A(1 ,"#RA-KFZ-SUMM");
@L2 N xRN_KFZ_STAT = N(2,0 ,"#RN-KFZ-STAT");
@L2 A xRA_KFZ_GSNA = A(10 ,"#RA-KFZ-GSNA");
@L2 A xRA_KFZ_BVNR = A(7 ,"#RA-KFZ-BVNR");
@L2 N xRN_KFZ_ANTR = N(8,0 ,"#RN-KFZ-ANTR");
@L2 N xRN_KFZ_VINK = N(8,0 ,"#RN-KFZ-VINK");
@L2 N xRN_KFZ_FPNR = N(3,0 ,"#RN-KFZ-FPNR");
@L2 A xRA_KFZ_MAPA = A(1 ,"#RA-KFZ-MAPA");
@L2 L xRL_KFZ_PRVV = L("#RL-KFZ-PRVV");
@L2 L xRL_KFZ_ST01 = L("#RL-KFZ-ST01");
@L2 L xRL_KFZ_AUTO = L("#RL-KFZ-AUTO");
@L2 A xRA_KFZ_OPI2 = A(3 ,"#RA-KFZ-OPI2");
@L2 L xRL_KFZ_BLND = L("#RL-KFZ-BLND");
@L2 C xRC_KFZ_BLND = C("#RC-KFZ-BLND");
@L2 A xRA_KFZ_ATXT = A(10 ,"#RA-KFZ-ATXT");
@L2 A xRA_KFZ_KTXT = A(3 ,"#RA-KFZ-KTXT");
@L2 A xRA_KFZ_AMNU = A(8 ,"#RA-KFZ-AMNU");
@L2 A xRA_KFZ_MENU = A(2 ,"#RA-KFZ-MENU");
@L1 A gISN = A(250 ,"+ISN");
@L1 Redefine gISN; // REDEF. BEGIN : +ISN
@L2 A xRA_ISN_ISN = A(5 ,"#RA-ISN-ISN");
@L2 Redefine xRA_ISN_ISN; // REDEF. BEGIN : #RA-ISN-ISN
@L3 P xRN_ISN_AKUN = P(9,0 ,"#RN-ISN-AKUN");
@L2 P xRN_ISN_HKUN = P(9,0 ,"#RN-ISN-HKUN");
@L2 P xRN_ISN_VKUN = P(9,0 ,"#RN-ISN-VKUN");
@L2 P xRN_ISN_ORTE = P(9,0 ,"#RN-ISN-ORTE");
@L2 P xRN_ISN_KORP = P(9,0 ,"#RN-ISN-KORP");
@L2 P xRN_ISN_KTAB = P(9,0 ,"#RN-ISN-KTAB");
@L2 P xRN_ISN_ABKU = P(9,0 ,"#RN-ISN-ABKU");
@L2 P xRN_ISN_OPOL = P(9,0 ,"#RN-ISN-OPOL");
@L2 P xRN_ISN_SPOL = P(9,0 ,"#RN-ISN-SPOL");
@L2 P xRN_ISN_OEVI = P(9,0 ,"#RN-ISN-OEVI");
@L2 P xRN_ISN_SEVI = P(9,0 ,"#RN-ISN-SEVI");
@L2 P xRN_ISN_OVV = P(9,0 ,"#RN-ISN-OVV");
@L2 P xRN_ISN_SVV = P(9,0 ,"#RN-ISN-SVV");
@L2 P xRN_ISN_OBU = P(9,0 ,"#RN-ISN-OBU");
@L2 P xRN_ISN_SBU = P(9,0 ,"#RN-ISN-SBU");
@L2 P xRN_ISN_BTS = P(9,0 ,"#RN-ISN-BTS");
@L2 P xRN_ISN_BTK = P(9,0 ,"#RN-ISN-BTK");
@L2 P xRN_ISN_EKPO = P(9,0 ,"#RN-ISN-EKPO");
@L2 P xRN_ISN_EKVV = P(9,0 ,"#RN-ISN-EKVV");
@L2 P xRN_ISN_EKK1 = P(9,0 ,"#RN-ISN-EKK1");
@L2 P xRN_ISN_OPOS = P(9,0 ,"#RN-ISN-OPOS");
@L2 P xRN_ISN_SPOS = P(9,0 ,"#RN-ISN-SPOS");
@L2 P xRN_ISN_EKG1 = P(9,0 ,"#RN-ISN-EKG1");
@L2 P xRN_ISN_HGPO = P(9,0 ,"#RN-ISN-HGPO");
@L2 P xRN_ISN_HGKO = P(9,0 ,"#RN-ISN-HGKO");
@L2 P xRN_ISN_OLS = P(9,0 ,"#RN-ISN-OLS");
@L2 P xRN_ISN_SLS = P(9,0 ,"#RN-ISN-SLS");
@L2 P xRN_ISN_HLS = P(9,0 ,"#RN-ISN-HLS");
@L2 P xRN_ISN_VDNR = P(9,0 ,"#RN-ISN-VDNR");
@L2 P xRN_ISN_MDST = P(9,0 ,"#RN-ISN-MDST");
@L2 P xRN_ISN_KTOS = P(9,0 ,"#RN-ISN-KTOS");
@L2 P xRN_ISN_KTOO = P(9,0 ,"#RN-ISN-KTOO");
@L2 P xRN_ISN_ORTS = P(9,0 ,"#RN-ISN-ORTS");
@L2 P xRN_ISN_FPNR = P(9,0 ,"#RN-ISN-FPNR");
@L2 P xRN_ISN_WA49 = P(9,0 ,"#RN-ISN-WA49");
@L2 P xRN_ISN_SD34 = P(8,0 ,"#RN-ISN-SD34");
@L2 P xRN_ISN_SPER = P(9,0 ,"#RN-ISN-SPER");
@L2 P xRN_ISN_MDS = P(9,0 ,"#RN-ISN-MDS");
@L2 P xRN_ISN_SUCH = P(9,0 ,"#RN-ISN-SUCH");
@L2 P xRN_ISN_ABST = P(9,0 ,"#RN-ISN-ABST");
@L2 P xRN_ISN_UMKON = P(9,0 ,"#RN-ISN-UMKON");
@L2 P xRN_ISN_MUBU = P(9,0 ,"#RN-ISN-MUBU");// MUTTERBUCHUNG (SK)
@L2 P xRN_ISN_RESERVE = P(9,0 ,"#RN-ISN-RESERVE").defineArray(range(1, 5));
@L1 A gKUN = A(250 ,"+KUN");
@L1 Redefine gKUN; // REDEF. BEGIN : +KUN
@L2 A xRA_KUN_KUNA = A(27 ,"#RA-KUN-KUNA");
@L2 A xRA_KUN_GHKZ = A(1 ,"#RA-KUN-GHKZ");
@L2 N xRN_KUN_GHK1 = N(8,0 ,"#RN-KUN-GHK1");
@L2 N xRN_KUN_GHK2 = N(8,0 ,"#RN-KUN-GHK2");
@L2 N xRN_KUN_DATV = N(8,0 ,"#RN-KUN-DATV");
@L2 N xRN_KUN_DATB = N(8,0 ,"#RN-KUN-DATB");
@L2 A xRA_KUN_BRAX = A(30 ,"#RA-KUN-BRAX");
@L2 N xRN_KUN_FILV = N(2,0 ,"#RN-KUN-FILV");
@L2 N xRN_KUN_FILB = N(2,0 ,"#RN-KUN-FILB");
@L2 N xRN_KUN_ANZEI = N(2,0 ,"#RN-KUN-ANZEI");
@L2 N xRN_KUN_BILKR = N(2,0 ,"#RN-KUN-BILKR");
@L2 N xRN_KUN_POLK = N(2,0 ,"#RN-KUN-POLK");
@L2 N xRN_KUN_POLB = N(2,0 ,"#RN-KUN-POLB");
@L2 A xRA_KUN_BANKKU = A(5 ,"#RA-KUN-BANKKU");
@L2 A xRA_KUN_BANKOR = A(27 ,"#RA-KUN-BANKOR");
@L2 N xRN_KUN_BANKLE = N(5,0 ,"#RN-KUN-BANKLE");
@L2 N xRN_KUN_TDAT = N(8,0 ,"#RN-KUN-TDAT");
@L2 N xRN_KUN_VR = N(3,0 ,"#RN-KUN-VR");
@L2 N xRN_KUN_VGVR = N(3,0 ,"#RN-KUN-VGVR");
@L2 N xRN_KUN_STAT = N(3,0 ,"#RN-KUN-STAT");
@L2 N xRN_KUN_HGV = N(2,0 ,"#RN-KUN-HGV");
@L2 A xRA_KUN_UG1V = A(2 ,"#RA-KUN-UG1V");
@L2 A xRA_KUN_UG2V = A(2 ,"#RA-KUN-UG2V");
@L2 A xRA_KUN_UG3V = A(2 ,"#RA-KUN-UG3V");
@L2 A xRA_KUN_SPV = A(1 ,"#RA-KUN-SPV");
@L2 N xRN_KUN_HGB = N(2,0 ,"#RN-KUN-HGB");
@L2 A xRA_KUN_UG1B = A(2 ,"#RA-KUN-UG1B");
@L2 A xRA_KUN_UG2B = A(2 ,"#RA-KUN-UG2B");
@L2 A xRA_KUN_UG3B = A(2 ,"#RA-KUN-UG3B");
@L2 A xRA_KUN_SPB = A(1 ,"#RA-KUN-SPB");
@L2 A xRA_KUN_LEER1 = A(38 ,"#RA-KUN-LEER1");
@L2 N xRN_KUN_OVGEM = N(5,0 ,"#RN-KUN-OVGEM");
@L2 N xRN_KUN_OVOKZ = N(6,0 ,"#RN-KUN-OVOKZ");
@L2 N xRN_KUN_OVOBK = N(4,0 ,"#RN-KUN-OVOBK");
@L2 A xRA_KUN_OVNAM = A(15 ,"#RA-KUN-OVNAM");
@L2 A xRA_KUN_LEER2 = A(4 ,"#RA-KUN-LEER2");
@L2 A xRA_KUN_OVFIL = A(2 ,"#RA-KUN-OVFIL");
@L2 A xRA_KUN_OVSA = A(1 ,"#RA-KUN-OVSA");
@L1 A gKEY = A(250 ,"+KEY");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 N xRN_KEY_VKKZ = N(8,0 ,"#RN-KEY-VKKZ");
@L2 A xRA_KEY_VVA = A(2 ,"#RA-KEY-VVA");
@L2 N xRN_KEY_VVNR = N(8,0 ,"#RN-KEY-VVNR");
@L2 N xRN_KEY_VHNR = N(3,0 ,"#RN-KEY-VHNR");
@L2 N xRN_KEY_VGES = N(3,0 ,"#RN-KEY-VGES");
@L2 N xRN_KEY_VRES = N(1,0 ,"#RN-KEY-VRES");
@L2 N xRN_KEY_VVIN = N(2,0 ,"#RN-KEY-VVIN");
@L2 N xRN_KEY_VBRA = N(2,0 ,"#RN-KEY-VBRA");
@L2 N xRN_KEY_VBRN = N(1,0 ,"#RN-KEY-VBRN");
@L2 A xRA_KEY_VBTART = A(1 ,"#RA-KEY-VBTART");
@L2 A xRA_KEY_VBSTAT = A(1 ,"#RA-KEY-VBSTAT");
@L2 N xRN_KEY_VUGES = N(3,0 ,"#RN-KEY-VUGES");
@L2 N xRN_KEY_VBJAHR = N(2,0 ,"#RN-KEY-VBJAHR");
@L2 N xRN_KEY_AKTONR = N(7,0 ,"#RN-KEY-AKTONR");
@L2 Redefine xRN_KEY_AKTONR; // REDEF. BEGIN : #RN-KEY-AKTONR
@L3 A xRA_KEY_VKTO = A(7 ,"#RA-KEY-VKTO");
@L2 A xRA_KEY_KOSEL = A(1 ,"#RA-KEY-KOSEL");
@L2 A xRA_KEY_LEER1 = A(4 ,"#RA-KEY-LEER1");
@L2 N xRN_KEY_AKKZ = N(8,0 ,"#RN-KEY-AKKZ");
@L2 A xRA_KEY_AVA = A(2 ,"#RA-KEY-AVA");
@L2 N xRN_KEY_AVNR = N(8,0 ,"#RN-KEY-AVNR");
@L2 N xRN_KEY_AHNR = N(3,0 ,"#RN-KEY-AHNR");
@L2 N xRN_KEY_AGES = N(3,0 ,"#RN-KEY-AGES");
@L2 N xRN_KEY_ARES = N(1,0 ,"#RN-KEY-ARES");
@L2 N xRN_KEY_AVIN = N(2,0 ,"#RN-KEY-AVIN");
@L2 N xRN_KEY_ABRA = N(2,0 ,"#RN-KEY-ABRA");
@L2 N xRN_KEY_ABRN = N(1,0 ,"#RN-KEY-ABRN");
@L2 A xRA_KEY_ABTART = A(1 ,"#RA-KEY-ABTART");
@L2 A xRA_KEY_ABSTAT = A(1 ,"#RA-KEY-ABSTAT");
@L2 N xRN_KEY_AUGES = N(3,0 ,"#RN-KEY-AUGES");
@L2 N xRN_KEY_ABJAHR = N(2,0 ,"#RN-KEY-ABJAHR");
@L2 N xRN_KEY_KTONR = N(7,0 ,"#RN-KEY-KTONR");
@L2 N xRN_KEY_PROV = N(2,0 ,"#RN-KEY-PROV");
@L2 N xRN_KEY_PROD = N(2,0 ,"#RN-KEY-PROD");
@L2 A xRA_KEY_PRKZ = A(1 ,"#RA-KEY-PRKZ");
@L2 N xRN_KEY_AFGS = N(3,0 ,"#RN-KEY-AFGS");
@L2 A xRA_KEY_AFPO = A(14 ,"#RA-KEY-AFPO");
@L2 N xRN_KEY_VFGS = N(3,0 ,"#RN-KEY-VFGS");
@L2 A xRA_KEY_VFPO = A(14 ,"#RA-KEY-VFPO");
@L2 N xRN_KEY_FBRA = N(2,0 ,"#RN-KEY-FBRA");
@L2 N xRN_KEY_NOTD = N(6,0 ,"#RN-KEY-NOTD");
@L2 Redefine xRN_KEY_NOTD;
@L3 N xRN_KEY_NOTD_YYYY = N(4,0 ,"#RN-KEY-NOTD-YYYY");
@L3 N xRN_KEY_NOTD_MM = N(2,0 ,"#RN-KEY-NOTD-MM");
@L2 N xRN_KEY_LSNR = N(2,0 ,"#RN-KEY-LSNR");
@L2 N xRN_KEY_KBJ = N(4,0 ,"#RN-KEY-KBJ");
@L2 N xRN_KEY_KBJNR = N(1,0 ,"#RN-KEY-KBJNR");
@L2 A xRA_KEY_ZNAM = A(31 ,"#RA-KEY-ZNAM");
@L2 A xRA_KEY_VNAM = A(2 ,"#RA-KEY-VNAM");
@L2 A xRA_KEY_STRA = A(1 ,"#RA-KEY-STRA");
@L2 N xRN_KEY_HAUS = N(4,0 ,"#RN-KEY-HAUS");
@L2 N xRN_KEY_POST = N(6,0 ,"#RN-KEY-POST");
@L2 A xRA_KEY_KZEX = A(1 ,"#RA-KEY-KZEX");
@L2 A xRA_KEY_ACHT = A(1 ,"#RA-KEY-ACHT");
@L2 N xRN_KEY_ALFA = N(2,0 ,"#RN-KEY-ALFA");
@L2 N xRN_KEY_BERU = N(5,0 ,"#RN-KEY-BERU");
@L2 A xRA_KEY_LEER2 = A(4 ,"#RA-KEY-LEER2");
@L2 A xRA_KEY_BRSL = A(1 ,"#RA-KEY-BRSL");
@L2 A xRA_KEY_PSTA = A(1 ,"#RA-KEY-PSTA");
@L2 A xRA_KEY_BRAW = A(1 ,"#RA-KEY-BRAW");
@L2 N xRN_KEY_BRVO = N(2,0 ,"#RN-KEY-BRVO");
@L2 N xRN_KEY_BRBI = N(2,0 ,"#RN-KEY-BRBI");
@L2 N xRN_KEY_JRVO = N(2,0 ,"#RN-KEY-JRVO");
@L2 N xRN_KEY_JRBI = N(2,0 ,"#RN-KEY-JRBI");
@L2 A xRA_KEY_VNAW = A(1 ,"#RA-KEY-VNAW");
@L2 A xRA_KEY_KEIG = A(1 ,"#RA-KEY-KEIG");
@L2 A xRA_KEY_SBID = A(3 ,"#RA-KEY-SBID");
@L2 A xRA_KEY_KURO = A(2 ,"#RA-KEY-KURO");
@L2 A xRA_KEY_PRAE = A(1 ,"#RA-KEY-PRAE");
@L2 A xRA_KEY_PROV = A(1 ,"#RA-KEY-PROV");
@L2 A xRA_KEY_OFFE = A(1 ,"#RA-KEY-OFFE");
@L2 A xRA_KEY_AKTU = A(1 ,"#RA-KEY-AKTU");
@L2 A xRA_KEY_ALLE = A(1 ,"#RA-KEY-ALLE");
@L2 A xRA_KEY_AOFA = A(1 ,"#RA-KEY-AOFA");
@L2 N xRN_KEY_BUDT = N(8,0 ,"#RN-KEY-BUDT");
@L2 P xRN_KEY_BUNR = P(9,0 ,"#RN-KEY-BUNR");
@L2 A xRA_KEY_VKZ = A(1 ,"#RA-KEY-VKZ");
@L2 A xRA_KEY_ABGA = A(1 ,"#RA-KEY-ABGA");
@L2 A xRA_KEY_SART = A(2 ,"#RA-KEY-SART");
@L2 A xRA_KEY_PFIL = A(2 ,"#RA-KEY-PFIL");
@L2 L xRL_KEY_UHR = L("#RL-KEY-UHR");
@L2 A xRA_KEY_STAT = A(1 ,"#RA-KEY-STAT");
@L2 A xRA_KEY_GSMT = A(1 ,"#RA-KEY-GSMT");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_1 = A(8 ,"#RA-KEY-08X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_VVO = A(22 ,"#RA-KEY-VVO");
@L2 A xRA_KEY_27X_1 = A(27 ,"#RA-KEY-27X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_AVO = A(22 ,"#RA-KEY-AVO");
@L2 A xRA_KEY_27X_2 = A(27 ,"#RA-KEY-27X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_GVO = A(22 ,"#RA-KEY-GVO");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_2 = A(8 ,"#RA-KEY-08X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_VBT = A(24 ,"#RA-KEY-VBT");
@L2 A xRA_KEY_24X = A(24 ,"#RA-KEY-24X"); // ZWISCHENRAUM
@L2 A xRA_KEY_ABT = A(24 ,"#RA-KEY-ABT");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_3 = A(8 ,"#RA-KEY-08X-3");// ZWISCHENRAUM
@L2 A xRA_KEY_VVK = A(16 ,"#RA-KEY-VVK");
@L2 A xRA_KEY_33X_1 = A(33 ,"#RA-KEY-33X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_AVK = A(16 ,"#RA-KEY-AVK");
@L2 A xRA_KEY_33X_2 = A(33 ,"#RA-KEY-33X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_GVK = A(16 ,"#RA-KEY-GVK");
@L1 A gSTV = A(250 ,"+STV");
@L1 Redefine gSTV; // REDEF. BEGIN : +STV
@L2 A xRA_STV_APRG = A(8 ,"#RA-STV-APRG");
@L2 A xRA_STV_VPRG = A(8 ,"#RA-STV-VPRG");
@L2 A xRA_STV_FUNK = A(4 ,"#RA-STV-FUNK");
@L2 Redefine xRA_STV_FUNK; // REDEF. BEGIN : #RA-STV-FUNK
@L3 N xRN_STV_FUNR = N(2,0 ,"#RN-STV-FUNR");
@L3 A xRA_STV_FUNR = A(2 ,"#RA-STV-FUNR");
@L2 N xRN_STV_AUSW = N(2,0 ,"#RN-STV-AUSW");
@L2 A xRA_STV_CODE = A(4 ,"#RA-STV-CODE"); // 30
@L2 N xRN_STV_OBNR = N(8,0 ,"#RN-STV-OBNR");
@L2 A xRA_STV_OART = A(2 ,"#RA-STV-OART");
@L2 N xRN_STV_HINR = N(3,0 ,"#RN-STV-HINR");
@L2 N xRN_STV_VINR = N(2,0 ,"#RN-STV-VINR");
@L2 N xRN_STV_BRAN = N(2,0 ,"#RN-STV-BRAN");
@L2 N xRN_STV_BRNR = N(1,0 ,"#RN-STV-BRNR");
@L2 N xRN_STV_GENR = N(3,0 ,"#RN-STV-GENR");
@L2 A xRA_STV_GMOD = A(1 ,"#RA-STV-GMOD");
@L2 A xRA_STV_AMOD = A(1 ,"#RA-STV-AMOD");
@L2 A xRA_STV_VIKZ = A(1 ,"#RA-STV-VIKZ");
@L2 A xRA_STV_DATI = A(8 ,"#RA-STV-DATI");
@L2 N xRN_STV_DATA = N(3,0 ,"#RN-STV-DATA");
@L2 A xRA_STV_OPID = A(3 ,"#RA-STV-OPID");
@L2 A xRA_STV_PFKY = A(96 ,"#RA-STV-PFKY");
@L2 Redefine xRA_STV_PFKY; // REDEF. BEGIN : #RA-STV-PFKY
@L3 A xRA_STV_PFF = A(4 ,"#RA-STV-PFF");
@L2 Redefine xRA_STV_PFKY; // REDEF. BEGIN : #RA-STV-PFKY
@L3 A xRA_STV_PF = A(4 ,"#RA-STV-PF").defineArray(range(1, 24));
@L2 N xRN_STV_DATN = N(6,0 ,"#RN-STV-DATN");
@L2 A xRA_STV_MMOD = A(3 ,"#RA-STV-MMOD");
@L1 Redefine gSTV; // REDEF. BEGIN : +STV
@L2 A xRA_STV_241X = A(241 ,"#RA-STV-241X");
@L2 A xRA_STV_APAR = A(1 ,"#RA-STV-APAR");
@L2 P xRN_STV_CURS = P(6,0 ,"#RN-STV-CURS");
@L2 P xRN_STV_MARK = P(3,0 ,"#RN-STV-MARK");
@L2 B xRN_STV_FNUM = B(2 ,"#RN-STV-FNUM");
@L1 A gKEY1 = A(250 ,"+KEY1");
@L1 Redefine gKEY1; // REDEF. BEGIN : +KEY1
@L2 A xRA_KEY1_ASSTART = A(4 ,"#RA-KEY1-ASSTART");
@L2 N xRN_KEY1_POSVI = N(2,0 ,"#RN-KEY1-POSVI");
@L2 N xRN_KEY1_POSVII = N(3,0 ,"#RN-KEY1-POSVII");
@L2 A xRA_KEY1_RVART = A(4 ,"#RA-KEY1-RVART");
@L2 N xRN_KEY1_RVVON = N(8,0 ,"#RN-KEY1-RVVON");
@L2 N xRN_KEY1_RVBHJAHR = N(4,0 ,"#RN-KEY1-RVBHJAHR");
@L2 N xRN_KEY1_RVBBNR = N(2,0 ,"#RN-KEY1-RVBBNR");
@L2 P xRN_KEY1_SBVANZ = P(1,0 ,"#RN-KEY1-SBVANZ");
@L2 P xRN_KEY1_SBVINR = P(2,0 ,"#RN-KEY1-SBVINR").defineArray(range(1, 2));
@L2 P xRN_KEY1_SBVII = P(2,0 ,"#RN-KEY1-SBVII").defineArray(range(1, 2));
@L2 N xRN_KEY1_BRA = N(2,0 ,"#RN-KEY1-BRA").defineArray(range(1, 3));
@L2 N xRN_KEY1_GEBDT = N(8,0 ,"#RN-KEY1-GEBDT");
@L2 A xRA_KEY1_2X = A(2 ,"#RA-KEY1-2X");
@L2 A xRA_KEY1_WAEHRUNG = A(3 ,"#RA-KEY1-WAEHRUNG");
@L2 A xRA_KEY1_SK_ALLE = A(1 ,"#RA-KEY1-SK-ALLE");
@L2 N xRN_KEY1_HNR = N(4,0 ,"#RN-KEY1-HNR");
@L2 A xRA_KEY1_MTVKZ = A(1 ,"#RA-KEY1-MTVKZ");
@L2 A xRA_KEY1_VIL = A(1 ,"#RA-KEY1-VIL");
@L2 A xRA_KEY1_OKZ = A(3 ,"#RA-KEY1-OKZ");
@L2 N xRN_KEY1_OPLZ = N(6,0 ,"#RN-KEY1-OPLZ");
@L2 N xRN_KEY1_OHNR = N(4,0 ,"#RN-KEY1-OHNR");
@L2 P xRN_KEY1_SUBVR = P(3,0 ,"#RN-KEY1-SUBVR");
@L2 P xRN_KEY1_SUBKTO = P(7,0 ,"#RN-KEY1-SUBKTO");
@L2 P xRN_KEY1_SUBZWST = P(3,0 ,"#RN-KEY1-SUBZWST");
@L2 A xRA_KEY1_SKART = A(3 ,"#RA-KEY1-SKART");
@L2 A xRA_KEY1_TCSART = A(1 ,"#RA-KEY1-TCSART");
@L2 A xRA_KEY1_TCZ1 = A(2 ,"#RA-KEY1-TCZ1");
@L2 Redefine xRA_KEY1_TCZ1; // REDEF. BEGIN : #RA-KEY1-TCZ1
@L3 N xRN_KEY1_TCZ1 = N(2,0 ,"#RN-KEY1-TCZ1");
@L2 P xRN_KEY1_TCODE = P(5,0 ,"#RN-KEY1-TCODE");
@L2 P xRN_KEY1_TCZ2 = P(2,0 ,"#RN-KEY1-TCZ2");
@L2 A xRA_KEY1_NATV = A(3 ,"#RA-KEY1-NATV");
@L2 A xRA_KEY1_NATO = A(3 ,"#RA-KEY1-NATO");
@L2 N xRN_KEY1_SNR = N(8,0 ,"#RN-KEY1-SNR");
@L2 N xRN_KEY1_LNR = N(5,0 ,"#RN-KEY1-LNR");
@L2 A xRA_KEY1_ZISB = A(3 ,"#RA-KEY1-ZISB");
@L2 N xRN_KEY1_STAT = N(5,0 ,"#RN-KEY1-STAT");
@L2 N xRN_KEY1_LZ = N(2,0 ,"#RN-KEY1-LZ");
@L2 A xRA_KEY1_GRDHA = A(2 ,"#RA-KEY1-GRDHA");
@L2 A xRA_KEY1_NATOV = A(3 ,"#RA-KEY1-NATOV");
@L2 N xRN_KEY1_AWDT = N(8,0 ,"#RN-KEY1-AWDT");
@L2 A xRA_KEY1_KOSP = A(1 ,"#RA-KEY1-KOSP");
@L2 N xRN_KEY1_EDSTAT = N(2,0 ,"#RN-KEY1-EDSTAT");
@L2 A xRA_KEY1_FREI = A(7 ,"#RA-KEY1-FREI");
@L2 N xRN_KEY1_KDAT = N(8,0 ,"#RN-KEY1-KDAT");
@L2 N xRN_KEY1_KGRD = N(2,0 ,"#RN-KEY1-KGRD");
@L2 N xRN_KEY1_PERSNR = N(5,0 ,"#RN-KEY1-PERSNR");
@L2 A xRA_KEY1_RPART = A(2 ,"#RA-KEY1-RPART");
@L2 N xRN_KEY1_RPDAT = N(8,0 ,"#RN-KEY1-RPDAT");
@L2 A xRA_KEY1_RVKZ = A(1 ,"#RA-KEY1-RVKZ");
@L2 A xRA_KEY1_RVKZ2 = A(1 ,"#RA-KEY1-RVKZ2");
@L2 A xRA_KEY1_LK = A(30 ,"#RA-KEY1-LK");
@L2 A xRA_KEY1_SCHEMA = A(2 ,"#RA-KEY1-SCHEMA");
@L2 N xRN_KEY1_RSOBNR = N(8,0 ,"#RN-KEY1-RSOBNR");
@L2 A xRA_KEY1_RSKZ = A(1 ,"#RA-KEY1-RSKZ");
@L2 A xRA_KEY1_BRFA = A(3 ,"#RA-KEY1-BRFA");
@L2 A xRA_KEY1_BUCHART = A(3 ,"#RA-KEY1-BUCHART");
@L2 N xRN_KEY1_BANKSCHL = N(3,0 ,"#RN-KEY1-BANKSCHL");
@L2 N xRN_KEY1_WERTEDAT = N(8,0 ,"#RN-KEY1-WERTEDAT");
@L2 A xRA_KEY1_VORPROG = A(8 ,"#RA-KEY1-VORPROG");
@L2 N xRN_KEY1_GILT_BIS = N(8,0 ,"#RN-KEY1-GILT-BIS");
@L2 P xRN_KEY1_FNR = P(3,0 ,"#RN-KEY1-FNR");
@L2 A xRA_KEY1_KO55MELD = A(1 ,"#RA-KEY1-KO55MELD");
@L1 A gKEY2 = A(250 ,"+KEY2");
@L1 Redefine gKEY2; // REDEF. BEGIN : +KEY2
@L2 A xRA_KEY2_TARIF = A(7 ,"#RA-KEY2-TARIF");
@L2 A xRA_KEY2_BANKNA = A(27 ,"#RA-KEY2-BANKNA");
@L2 A xRA_KEY2_USERGRUPPE = A(8 ,"#RA-KEY2-USERGRUPPE");
@L2 N xRN_KEY2_KOSTST = N(5,0 ,"#RN-KEY2-KOSTST");
@L2 A xRA_KEY2_AVISO_OP = A(3 ,"#RA-KEY2-AVISO-OP");
@L2 N xRN_KEY2_AVISO_DT = N(8,0 ,"#RN-KEY2-AVISO-DT");
@L2 N xRN_KEY2_AVISO_GD = N(3,0 ,"#RN-KEY2-AVISO-GD");
@L2 N xRN_KEY2_AVISO_DTB = N(8,0 ,"#RN-KEY2-AVISO-DTB");
@L2 A xRA_KEY2_SPRACH_KZ = A(2 ,"#RA-KEY2-SPRACH-KZ");
@L2 A xRA_KEY2_GEWINN_VB = A(4 ,"#RA-KEY2-GEWINN-VB");
@L2 N xRN_KEY2_GEWINN_JAHR = N(4,0 ,"#RN-KEY2-GEWINN-JAHR");
@L2 A xRA_KEY2_ABST_SA = A(2 ,"#RA-KEY2-ABST-SA");
@L2 A xRA_KEY2_ABST_ZA = A(1 ,"#RA-KEY2-ABST-ZA");
@L2 P xRN_KEY2_ABST_ZR = P(9,0 ,"#RN-KEY2-ABST-ZR");
@L2 A xRA_KEY2_ABST_NK = A(1 ,"#RA-KEY2-ABST-NK");
@L2 A xRA_KEY2_ABST_NA = A(8 ,"#RA-KEY2-ABST-NA");
@L2 A xRA_KEY2_FAB = A(3 ,"#RA-KEY2-FAB");
@L2 A xRA_KEY2_TYP = A(10 ,"#RA-KEY2-TYP");
@L2 A xRA_KEY2_FZA = A(1 ,"#RA-KEY2-FZA");
@L2 N xRN_KEY2_LST = N(1,0 ,"#RN-KEY2-LST");
@L2 N xRN_KEY2_KTBJ = N(4,0 ,"#RN-KEY2-KTBJ");
@L2 N xRN_KEY2_KW = N(3,0 ,"#RN-KEY2-KW");
@L2 P xRN_KEY2_UEBWNR = P(6,0 ,"#RN-KEY2-UEBWNR");
@L2 P xRN_KEY2_UNTNR = P(4,0 ,"#RN-KEY2-UNTNR");
@L2 A xRA_KEY2_SERIE = A(1 ,"#RA-KEY2-SERIE");
@L2 P xRN_KEY2_PAKNR = P(9,0 ,"#RN-KEY2-PAKNR");
@L2 A xRA_KEY2_WHG_VON = A(3 ,"#RA-KEY2-WHG-VON");
@L2 A xRA_KEY2_WHG_NACH = A(3 ,"#RA-KEY2-WHG-NACH");
@L2 A xRA_KEY2_EXTSA = A(3 ,"#RA-KEY2-EXTSA");
@L2 N xRN_KEY2_GKNR = N(8,0 ,"#RN-KEY2-GKNR");
@L1 C xCV_WHBS = C("#CV-WHBS"); // GW FUER WIEDERHOLTE BS-AUSG.
@L1 C xCV_OUTP = C("#CV-OUTP"); // PROTECT BEI A+B - PROGRAMMEN
@L1 C xCV_STV = C("#CV-STV"); // KEY-FELDER STEUERLEISTE
@L1 L xWL_KEYWECHS = L("#WL-KEYWECHS"); // SCHALTER, OB KEYWECHSEL
@L1 L xWL_INTERN = L("#WL-INTERN"); // SCHALTER, OB BLINDER DURCHL.
@L1 A xWA_HELP_TASTE = A(4 ,"#WA-HELP-TASTE"); // PF-TASTE FUER 'HELP'
@L1 A xWA_MENU_TASTE = A(4 ,"#WA-MENU-TASTE"); // PF-TASTE FUER 'MENU'
@L1 P xWN_SEL_ISN = P(9,0 ,"#WN-SEL-ISN").defineArray(range(1,24));
@L1 A xWA_FUNKTION = A(5 ,"#WA-FUNKTION").defineArray(range(1,100));// FUNKTIONSTABELLE
@L1 A xWA_VORWEG_AUFRUF = A(8 ,"#WA-VORWEG-AUFRUF");// VORWEG-AUFRUF (Z.B. SCHWEBE)
@L1 L xWL_TEST = L("#WL-TEST"); // KENNZEICHEN, OB TEST
@L1 N xWN_WARTEN_SUMME = N(5,0 ,"#WN-WARTEN-SUMME");// ZAEHLER FUER SUSPEND
@L1 A xWA_AUSWAHL_KZ = A(1 ,"#WA-AUSWAHL-KZ").defineArray(range(1,24));// MARKIERUNGSLEISTE
@L1 N xWN_AUSWAHL_ANZAHL = N(2,0 ,"#WN-AUSWAHL-ANZAHL");
@L1 A xWA_AUSWAHL_GUELTIG = A(20 ,"#WA-AUSWAHL-GUELTIG");
/*
* Platz für neue globale Variable
* Definitionen in:
* YLNEUGLO/YCNEUGLO
*/
@L1 A gNEUGLO_01 = A(250 ,"+NEUGLO-01");
@L1 Redefine gNEUGLO_01;
@L2 A xBTC_STEUERUNG = A(8 ,"#BTC-STEUERUNG");
@L2 A xUMS_MIS_BILANZKREIS = A(1 ,"#UMS-MIS-BILANZKREIS").defineArray(range(1, 7));
@L2 A xUMS_MIS_BRAN = A(2 ,"#UMS-MIS-BRAN").defineArray(range(1, 10));
@L2 P xKEY1_SUCH_BETRAG = P(9,2 ,"#KEY1-SUCH-BETRAG");
@L2 P xKEY2_ARCHIV_DB = P(3,0 ,"#KEY2-ARCHIV-DB");
@L2 A xKEY2_EXTKZ = A(1 ,"#KEY2-EXTKZ");
@L2 N xRN_KEY_JAHRV = N(4,0 ,"#RN-KEY-JAHRV");
@L2 N xRN_KEY_JAHRB = N(4,0 ,"#RN-KEY-JAHRB");
@L2 A xRA_KEY_EXT_GRUND_NAME = A(31 ,"#RA-KEY-EXT-GRUND-NAME");
@L2 P xRN_KEY_EXT_BB_VON = P(8,0 ,"#RN-KEY-EXT-BB-VON");
@L2 P xRN_KEY_EXT_BB_BIS = P(8,0 ,"#RN-KEY-EXT-BB-BIS");
@L2 P xUMS_MA_SUB_SATZART_1 = P(3,0 ,"#UMS-MA-SUB-SATZART-1");
@L2 A xRA_KEY_EXT_ANTRAGSART = A(3 ,"#RA-KEY-EXT-ANTRAGSART");
@L2 P xFLV_TRANS = P(7,0 ,"#FLV-TRANS");
@L2 A xFLV_ISIN = A(12 ,"#FLV-ISIN");
@L2 P xFLV_BUNR = P(7,0 ,"#FLV-BUNR");
@L2 P xFLV_GILT_BIS = P(8,0 ,"#FLV-GILT-BIS");
@L2 P xFLV_DATUM = P(8,0 ,"#FLV-DATUM");
@L2 P xFLV_VV_VINR = P(3,0 ,"#FLV-VV-VINR");
@L2 P xFLV_VV_BRAN = P(3,0 ,"#FLV-VV-BRAN");
@L2 P xFLV_VV_BRNR = P(3,0 ,"#FLV-VV-BRNR");
@L2 P xFLV_VV_HINR = P(3,0 ,"#FLV-VV-HINR");
@L2 A xVLK_EXVERARB = A(30 ,"#VLK-EXVERARB");
@L2 P xRN_KEY_IVKNR = P(8,0 ,"#RN-KEY-IVKNR");
@L2 A xRA_SC_MELDUNGS_ART = A(1 ,"#RA-SC-MELDUNGS-ART");
@L2 A xRA_OS_GFALL = A(2 ,"#RA-OS-GFALL");
@L2 A xRA_OS_DOKART = A(4 ,"#RA-OS-DOKART");
@L2 A xRA_OS_BR_ART = A(3 ,"#RA-OS-BR-ART");
@L2 N xRN_KEY_YU_PERSNR = N(13,0 ,"#RN-KEY-YU-PERSNR");
@L2 N xSCN_VERS_BEGINN = N(8,0 ,"#SCN-VERS-BEGINN");
@L2 A xDI_ABTEILUNG = A(8 ,"#DI-ABTEILUNG");
@L2 P xDI_STAT_DZT = P(3,0 ,"#DI-STAT-DZT");
@L2 P xDI_STAT_DZT_BIS = P(3,0 ,"#DI-STAT-DZT-BIS");
@L2 A xDI_SPARTE = A(2 ,"#DI-SPARTE");
@L2 A xDI_STATION = A(8 ,"#DI-STATION");
@L2 A xDI_WM_OPID = A(8 ,"#DI-WM-OPID");
@L2 A xDI_AW_POL_FIL = A(1 ,"#DI-AW-POL-FIL");
@L2 A xDI_VAE_ART = A(3 ,"#DI-VAE-ART");
@L2 A xDI_WK_VAE_ART = A(3 ,"#DI-WK-VAE-ART");
@L2 A xDI_AW_SUB_VR = A(1 ,"#DI-AW-SUB-VR");
@L2 P xDI_SUB_VR = P(3,0 ,"#DI-SUB-VR");
@L2 P xDI_POL_KREIS = P(2,0 ,"#DI-POL-KREIS");
@L2 P xDI_POL_BRAN = P(2,0 ,"#DI-POL-BRAN");
@L2 A xDI_VAE_HINW = A(3 ,"#DI-VAE-HINW");
@L1 A gNEUGLO_02 = A(250 ,"+NEUGLO-02");
@L1 Redefine gNEUGLO_02;
@L2 A xDI_BEARB_GRP = A(3 ,"#DI-BEARB-GRP");
@L2 A xDI_AW_DRF = A(1 ,"#DI-AW-DRF");
@L2 A xDI_AW_E_ANT = A(1 ,"#DI-AW-E-ANT");
@L2 P xDI_PC_ANT_VKTO = P(7,0 ,"#DI-PC-ANT-VKTO");
@L2 P xDI_PLZ_VON = P(7,0 ,"#DI-PLZ-VON");
@L2 P xDI_PLZ_BIS = P(7,0 ,"#DI-PLZ-BIS");
@L2 A xDI_POSTNATION = A(3 ,"#DI-POSTNATION");
@L2 P xDI_WM_DATUM = P(8,0 ,"#DI-WM-DATUM");
@L2 P xDI_POL_FILIALE = P(2,0 ,"#DI-POL-FILIALE");
@L2 A xDI_AW_VAE_HINW = A(1 ,"#DI-AW-VAE-HINW");
@L2 A xDI_AW_VKTO = A(1 ,"#DI-AW-VKTO");
@L2 A xDI_AW_BEARB_GRP = A(1 ,"#DI-AW-BEARB-GRP");
@L2 A xDI_AW_KR_BR = A(1 ,"#DI-AW-KR-BR");
@L2 A xDI_AW_VAE_ART = A(1 ,"#DI-AW-VAE-ART");
@L2 P xDI_VR_KEY = P(3,0 ,"#DI-VR-KEY");
@L2 A xDI_OART = A(2 ,"#DI-OART");
@L2 P xDI_WM_DATUM_VON = P(8,0 ,"#DI-WM-DATUM-VON");
@L2 A xDI_WM_AUSWAHL = A(1 ,"#DI-WM-AUSWAHL");
@L2 A xRA_KEY_OIB = A(20 ,"#RA-KEY-OIB");
}

View File

@@ -0,0 +1,23 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package CSTENTW;
import innowake.mee.natural.api.*;
/**
* IT fixture (roadmap item 247), shaped like the corpus's CSTENTW/DISKRECH.java: a program in one
* library that includes the GDA of another. COMMON_DA exists in KORIN and ABFRAGE; only the qualifier
* says which one this module means.
*/
@Natural(moduleName="DISKTEST")
public class DISKTEST extends NaturalProgram {
@Using KORIN.COMMON_DA COMMON_DA = globalUsing(KORIN.COMMON_DA.class);
@Using KORIN.Mitarbeiter.Verwaltung.YPUMPRUE_DA YPUMPRUE_DA = localUsing(KORIN.Mitarbeiter.Verwaltung.YPUMPRUE_DA.class);
@Using KORIN.NOFILE_DA NOFILE_DA = localUsing(KORIN.NOFILE_DA.class);
@Override
public void run() {
}
}

View File

@@ -0,0 +1,22 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN;
import innowake.mee.natural.api.*;
/**
* IT fixture (roadmap item 247): the unqualified form. COMMON_DA is named without a package, so only
* Java's same-package rule — this file's own package, KORIN — can pick KORIN.COMMON over
* ABFRAGE.COMMON.
*/
@Natural(moduleName="UMKTEST")
public class UMKTEST extends NaturalProgram {
@Using COMMON_DA COMMON_DA = globalUsing(COMMON_DA.class);
@Using KORIN.Mitarbeiter.Verwaltung.YPUMPRUE_DA YPUMPRUE_DA = localUsing(KORIN.Mitarbeiter.Verwaltung.YPUMPRUE_DA.class);
@Override
public void run() {
}
}

View File

@@ -0,0 +1,113 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN.Mitarbeiter.Verwaltung;
import innowake.mee.natural.api.*;
/**
* <pre>
* *LCM-HEADER BEGIN ****** $$LCMHEADER:20140410-V00.01**********
* * header created during migration process *
* **************************************************************
* * @MERGE-SOURCE N
* * @NO-MERGE N
* * @FUNCTION Rule : Vkto-Daten
* * @OWNER P07
* **************************************************************
* **************************************************************
* *LCM-HEADER END ****** $$LCMHEADER:20140410-V00.01**********
* </pre>
*/
@Natural(moduleName="V-MIT04")
public class V_MIT04 extends NaturalSubprogram {
//
// VKTO-Daten zu Original od. Schwebe
//
@Using KORIN.Diverse.Framework.GWSTGP_DA GWSTGP_DA = parameterUsing(KORIN.Diverse.Framework.GWSTGP_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Framework.GWSTGP_DA) */
public final KORIN.Diverse.Framework.GWSTGP_DA.GWSTGP GWSTGP = GWSTGP_DA.GWSTGP;
/* @@USING:END(KORIN.Diverse.Framework.GWSTGP_DA) */
@Using KORIN.Diverse.Framework.GWPDA_M_DA GWPDA_M_DA = parameterUsing(KORIN.Diverse.Framework.GWPDA_M_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Framework.GWPDA_M_DA) */
public final KORIN.Diverse.Framework.GWPDA_M_DA.MSG_INFO MSG_INFO = GWPDA_M_DA.MSG_INFO;
/* @@USING:END(KORIN.Diverse.Framework.GWPDA_M_DA) */
@Using V_MIT04P_DA V_MIT04P_DA = parameterUsing(V_MIT04P_DA.class);
/* @@USING:BEGIN(V_MIT04P_DA) */
public final V_MIT04P_DA.V_MIT04P V_MIT04P = V_MIT04P_DA.V_MIT04P;
/* @@USING:END(V_MIT04P_DA) */
@Using KORIN.Diverse.Framework.GWLOGNAM_DA GWLOGNAM_DA = localUsing(KORIN.Diverse.Framework.GWLOGNAM_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Framework.GWLOGNAM_DA) */
public final KORIN.Diverse.Framework.GWLOGNAM_DA.xSTD_RETCODE xSTD_RETCODE = GWLOGNAM_DA.xSTD_RETCODE;
public final KORIN.Diverse.Framework.GWLOGNAM_DA.xSTD_LOOPCODE xSTD_LOOPCODE = GWLOGNAM_DA.xSTD_LOOPCODE;
/* @@USING:END(KORIN.Diverse.Framework.GWLOGNAM_DA) */
/* * */
// Daten für: MITZ1R
@Using MITZ1RP_DA MITZ1RP_DA = localUsing(MITZ1RP_DA.class);
/* @@USING:BEGIN(MITZ1RP_DA) */
public final MITZ1RP_DA.MITZ1RR MITZ1RR = MITZ1RP_DA.MITZ1RR;
public final MITZ1RP_DA.MITZ1RP MITZ1RP = MITZ1RP_DA.MITZ1RP;
/* @@USING:END(MITZ1RP_DA) */
@Local
@L1 Group xVON_MITZ1R = group("#VON-MITZ1R"); // <PRIME-KEY-VON>
@L2 A xGESAMT = A(7 ,"#GESAMT");
@L2 Redefine xGESAMT;
@L3 A SVKTO = A(7 ,"SVKTO");
@L3 Redefine SVKTO;
@L4 P VR_KEY = P(3,0 ,"VR-KEY");
@L4 P VKTO = P(7,0 ,"VKTO");
@L4 A STATUS_BYTE = A(1 ,"STATUS-BYTE");
/* * */
@Override
public void run() {
/* * */
reset(V_MIT04P.OUTPUTS);
/* * */
if (((NE(V_MIT04P.xVR_KEY, 0)
&& NE(V_MIT04P.xVKTO, 0))
&& NE(V_MIT04P.xSTATUS_BYTE, " "))) {
assign(xVON_MITZ1R.VR_KEY, V_MIT04P.xVR_KEY);
assign(xVON_MITZ1R.VKTO, V_MIT04P.xVKTO);
assign(xVON_MITZ1R.STATUS_BYTE, V_MIT04P.xSTATUS_BYTE);
//
//
assign(MITZ1RR.xVON_KEY, xVON_MITZ1R.xGESAMT);
assign(MITZ1RR.xFUNCTION, "Read");
callnat("MITZ1R", GWSTGP, MSG_INFO, MITZ1RR, MITZ1RP);
if (NE(MSG_INFO.xxRETURN_CODE, xSTD_RETCODE.OK)) {
escapeRoutine();
}
if ((NE(MITZ1RR.xSW_FOUND, "J")
&& EQ(V_MIT04P.xSCHWEBE_GILT, "J"))) {
assign(xVON_MITZ1R.STATUS_BYTE, "S");
assign(MITZ1RR.xVON_KEY, xVON_MITZ1R.xGESAMT);
assign(MITZ1RR.xFUNCTION, "Read");
callnat("MITZ1R", GWSTGP, MSG_INFO, MITZ1RR, MITZ1RP);
if (NE(MSG_INFO.xxRETURN_CODE, xSTD_RETCODE.OK)) {
escapeRoutine();
}
}
if (EQ(MITZ1RR.xSW_FOUND, "J")) {
assign(V_MIT04P.xZS_NAME, MITZ1RP.ZS_NAME.get(1));
assign(V_MIT04P.xVKTO_ORGBEZEICHNUNG, MITZ1RP.VKTO_ORGBEZEICHNUNG);
assign(V_MIT04P.xVKTO_STATUS_TEXT, MITZ1RP.VKTO_STATUS_TEXT);
assign(V_MIT04P.xVKTO_VERWENDUNG, MITZ1RP.VKTO_VERWENDUNG);
assign(V_MIT04P.xVKTO_VERWENDUNG_TEXT, MITZ1RP.VKTO_VERWENDUNG_TEXT);
if (GT(V_MIT04P.xVKTO_ORGBEZEICHNUNG, " ")) {
assign(V_MIT04P.xMAP_NAME, V_MIT04P.xVKTO_ORGBEZEICHNUNG);
} else {
assign(V_MIT04P.xMAP_NAME, V_MIT04P.xZS_NAME);
}
} else {
// KONTONUMMER :1: NICHT IN MITARBEITER-DATEI GESPEICHERT
assign(MSG_INFO.xxMSG_NR, 684);
assign(MSG_INFO.xxMSG_DATA.get(1), V_MIT04P.xVKTO);
escapeRoutine();
}
}
/* * */
}
}

View File

@@ -0,0 +1,24 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN.Mitarbeiter.Verwaltung;
import innowake.mee.natural.api.*;
@Natural(moduleName="YPUMPRUE")
public class YPUMPRUE_DA extends NaturalData {
@Parameter
@L1 Group YPUMPRUE = group("YPUMPRUE");
@L2 Group INPUTS = group("INPUTS");
@L3 N VR_KEY = N(3,0 ,"VR-KEY");
@L3 N VKTO = N(7,0 ,"VKTO");
@L3 N VKTO_ZIEL = N(7,0 ,"VKTO-ZIEL");
@L3 A KZ_PR_UMK_ABWEICH_KVA = A(1 ,"KZ-PR-UMK-ABWEICH-KVA");
@L2 Group OUTPUTS = group("OUTPUTS");
@L3 A VKTO_NAME = A(31 ,"VKTO-NAME");
@L3 N FNUM = N(4,0 ,"FNUM");
@L3 A FEHLER = A(78 ,"FEHLER");
}

View File

@@ -0,0 +1,652 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package ABFRAGE;
import innowake.mee.natural.api.*;
@Natural(moduleName="COMMON")
public class COMMON_DA extends NaturalData {
@Global
/*
* GDA für LIBRARY KORIN INCLUSIVE
* REDEFINITIONEN
*/
@L1 A gPROGRAMM = A(8 ,"+PROGRAMM");
@L1 A gBPOL_PARM = A(17 ,"+BPOL-PARM");
@L1 N gBBST_POLNR = N(9,0 ,"+BBST-POLNR");
@L1 A gBBST_ART = A(9 ,"+BBST-ART");
@L1 A gABTEILUNG = A(5 ,"+ABTEILUNG");
@L1 A gPASSWORD = A(8 ,"+PASSWORD");
@L1 A gGETMAIL = A(1 ,"+GETMAIL");
@L1 A gERROR_PROGRAM = A(8 ,"+ERROR-PROGRAM");
@L1 A gERROR_STAT = A(1 ,"+ERROR-STAT");
@L1 N gERROR_LINE = N(4,0 ,"+ERROR-LINE");
@L1 N gERROR_NR = N(4,0 ,"+ERROR-NR");
@L1 A gERGEBNIS = A(79 ,"+ERGEBNIS");
@L1 A gSPOOL_PARM = A(80 ,"+SPOOL-PARM");
@L1 Redefine gSPOOL_PARM;
@L2 A xSPUSER = A(8 ,"#SPUSER");
@L2 A xSPDEST = A(8 ,"#SPDEST");
@L2 A xSPFORM = A(1 ,"#SPFORM");
@L1 A gINIT_VAR = A(240 ,"+INIT-VAR");
@L1 Redefine gINIT_VAR; // REDEF. BEGIN : +INIT-VAR
@L2 A xRA_INITVAR_GETMAIL = A(1 ,"#RA-INITVAR-GETMAIL");// GETMAIL duchgeführt Y/N
@L2 N xINIT_DATIME = N(15,0 ,"#INIT-DATIME");// STARTUP Datum/Zeitder
@L2 Redefine xINIT_DATIME; // letzten Startup-Durchführung
@L3 N xINIT_DATE = N(8,0 ,"#INIT-DATE");// "YYYYMMDDHHMMSSZ"
@L3 Redefine xINIT_DATE;
@L4 A xINIT_DATE_ALPHA = A(8 ,"#INIT-DATE-ALPHA");
@L3 N xINIT_TIME = N(7,0 ,"#INIT-TIME");
@L3 Redefine xINIT_TIME;
@L4 A xINIT_TIME_ALPHA = A(7 ,"#INIT-TIME-ALPHA");
@L2 A xINIT_USER = A(8 ,"#INIT-USER"); // STARTUP U-ID d. Current LOGON
@L2 A xINIT_ID = A(8 ,"#INIT-ID"); // STARTUP Terminal-ID
@L2 A xINIT_GROUP = A(8 ,"#INIT-GROUP"); // STARTUP GROUP-ID, über die
/* /* gelinkt */
@L2 A xSTARTUP_PROGRAM = A(8 ,"#STARTUP-PROGRAM");// STARTUP Startup-Programm
@L2 A xRESTART_PROGRAM = A(8 ,"#RESTART-PROGRAM");// STARTUP Restart-Programm
@L2 A xERROR_PROGRAM = A(8 ,"#ERROR-PROGRAM");// #ERR letztes fehlerhaftes PRG
/* /* STARTUP Error-Programm */
@L2 A xRA_INITVAR_CICS_NAME = A(8 ,"#RA-INITVAR-CICS-NAME");// STARTUP und APPLID
@L2 Redefine xRA_INITVAR_CICS_NAME; // "CSWVCICS"/"CSWVCICT"
@L3 A xCICSNAME = A(8 ,"#CICSNAME"); // Redef. aus Namensgründen
@L2 Redefine xRA_INITVAR_CICS_NAME;
@L3 A xCICS_NAME = A(8 ,"#CICS-NAME"); // Redef. aus Namensgründen
@L2 A FILLER = A(8 ,"FILLER");
@L2 A xUSER_NAME = A(32 ,"#USER-NAME"); // STARTUP User-Name
@L2 A xAPPL_NAME = A(32 ,"#APPL-NAME"); // STARTUP Anwendungs-Name
@L2 A xSYS_SCHALTER = A(8 ,"#SYS-SCHALTER");// DIV SYSTEM-Schalter
@L2 Redefine xSYS_SCHALTER;
@L3 A xSYS_RESTART = A(1 ,"#SYS-RESTART");// #ERR bei Absturz Restart Y/N
@L3 A xSYS_CANCEL = A(1 ,"#SYS-CANCEL");// #CAN BATCH CANCELNin #ERROR
@L3 A xERROR_STAT = A(1 ,"#ERROR-STAT");// #ERR letzter Fehler-Status
@L3 A xERROR_OPTN = A(1 ,"#ERROR-OPTN");// #ERR RETRY ON ERRORS Y/N
@L3 A xERROR_SAME = A(1 ,"#ERROR-SAME");// #ERR same ERROR wie zuvor Y/N
/* 0=NEIN 1=PROG 2=NUMMER 3=BEIDE */
@L3 A xERROR_LOG = A(1 ,"#ERROR-LOG"); // #ERR N=kein ERROR-Logging
@L3 A xMONITOR = A(1 ,"#MONITOR"); // STARTUP B=BATCH, C=CICS
/* /* P=COMPLETE */
@L3 A FILLER2 = A(1 ,"FILLER2");
@L2 A xERROR_PROG = A(8 ,"#ERROR-PROG");
@L2 N xERROR_NR = N(4,0 ,"#ERROR-NR"); // #ERR letzte Fehlernummer
@L2 Redefine xERROR_NR;
@L3 A xERRAX = A(4 ,"#ERRAX");
@L2 N xERROR_LINE = N(4,0 ,"#ERROR-LINE"); // #ERR letzte fehlerhafte Zeile
@L2 A xMESSAGE = A(72 ,"#MESSAGE"); // #ERR Fehlermeldung
@L2 Redefine xMESSAGE;
@L3 A xMANF = A(1 ,"#MANF");
@L3 A STAR = A(1 ,"STAR");
@L3 A RESTMSG = A(70 ,"RESTMSG");
@L1 A gBERECHTIGUNGEN = A("+BERECHTIGUNGEN");
@L1 A gVSU = A(250 ,"+VSU");
@L1 A gKIS = A(250 ,"+KIS");
@L1 Redefine gKIS; // REDEF. BEGIN : +KIS
@L2 N xRN_KIS_ZAHL = N(3,0 ,"#RN-KIS-ZAHL");
@L2 B xRA_KIS_KUISNT = B(3 ,"#RA-KIS-KUISNT").defineArray(range(1, 20));
@L2 Redefine xRA_KIS_KUISNT; // REDEF. BEGIN : #RA-KIS-KUISNT
@L3 B xRA_KIS_KUISN = B(3 ,"#RA-KIS-KUISN");
@L2 N xRN_KIS_OVZL = N(3,0 ,"#RN-KIS-OVZL");
@L2 B xRA_KIS_OVISN = B(3 ,"#RA-KIS-OVISN").defineArray(range(1, 60));
@L1 A gUMS = A(250 ,"+UMS");
@L1 Redefine gUMS; // REDEF. BEGIN : +UMS
@L2 A xRA_UMS_CODE = A(8 ,"#RA-UMS-CODE");
@L2 P xRN_UMS_LAST_VR = P(3,0 ,"#RN-UMS-LAST-VR");
@L2 A xRA_UMS_MD_KZ1_BKLZ_KZ = A(1 ,"#RA-UMS-MD-KZ1-BKLZ-KZ");
@L2 A xRA_UMS_MD_KZ1_TARIF = A(1 ,"#RA-UMS-MD-KZ1-TARIF");
@L2 A xRA_UMS_WAEHRUNG_MD = A(3 ,"#RA-UMS-WAEHRUNG-MD");
@L2 N xRN_UMS_LAST_LOAD = N(6,0 ,"#RN-UMS-LAST-LOAD");
@L2 A xUMS_VORG_OPID = A(3 ,"#UMS-VORG-OPID");
@L2 A xUMS_FORCE = A(1 ,"#UMS-FORCE"); // Beharrung auf eigene
/* * /* PF-Tasten-Belegung */
@L2 N xUMS_SNR_FIL = N(2,0 ,"#UMS-SNR-FIL");// Schaden-Filial-Nummer
@L2 A xUMS_SNR_KREIS = A(2 ,"#UMS-SNR-KREIS");// Schaden-Kreis zB.:'SA'
@L2 N xUMS_SNR_JAHR = N(4,0 ,"#UMS-SNR-JAHR");// Schaden-Jahr
@L2 N xUMS_SNR_LFD_NR = N(6,0 ,"#UMS-SNR-LFD-NR");// Schaden-Laufende-Nummer
@L2 A xUMS_SNR_STATUS = A(1 ,"#UMS-SNR-STATUS");// Status-Byte 'O','S','H'
@L2 A xUMS_ART = A(2 ,"#UMS-ART"); // Schadenart
@L2 A xUMS_REFERENT = A(8 ,"#UMS-REFERENT");// Referent
@L2 A xUMS_EVI_TYP = A(1 ,"#UMS-EVI-TYP"); // eigen oder fremd-Vi
@L2 P xUMS_IDENTCOUNTER = P(5,0 ,"#UMS-IDENTCOUNTER");
@L2 P xUMS_OUTPUTNR = P(5,0 ,"#UMS-OUTPUTNR");
@L2 P xUMS_EREIGNIS_DT = P(8,0 ,"#UMS-EREIGNIS-DT");// nur für LV (= Stornodatum)
@L2 P xUMS_SCN_HIST = P(3,0 ,"#UMS-SCN-HIST");// merken Hist-Lesestand
@L2 B xUMS_ANZEIGEDAT_KPL = B(3 ,"#UMS-ANZEIGEDAT-KPL");// für Aufruf SK10
@L2 N xUMS_SKT_FIL = N(2,0 ,"#UMS-SKT-FIL");// Filial-Nummer Kontostamm
@L2 A xUMS_SKT_KREIS = A(2 ,"#UMS-SKT-KREIS");// Schaden-Kreis Kontostamm
@L2 N xUMS_SKT_JAHR = N(4,0 ,"#UMS-SKT-JAHR");// Schaden-Jahr Kontostamm
@L2 N xUMS_SKT_LFD_NR = N(6,0 ,"#UMS-SKT-LFD-NR");// Schaden-Lfd-Nr Kontostamm
@L2 N xUMS_SKT_BRAN = N(2,0 ,"#UMS-SKT-BRAN");// Schaden-Branche Kontostamm
@L2 A xUMS_WORKSTATION_ID = A(8 ,"#UMS-WORKSTATION-ID");// Workstation (OD) *** frei ***
@L2 A xRA_UMS_BRGR = A(1 ,"#RA-UMS-BRGR").defineArray(range(1,3));
@L2 A xUMS_WF_ABT = A(8 ,"#UMS-WF-ABT"); // Workflow-Abteilung(für OD)
@L2 A xUMS_WF_STAT = A(8 ,"#UMS-WF-STAT"); // Workflow-Station(für OD)
@L2 A xUMS_WF_OPID = A(8 ,"#UMS-WF-OPID"); // Workflow-Opid(für OD)
@L2 P xUMS_WF_SORT_DOKU_ART = P(3,0 ,"#UMS-WF-SORT-DOKU-ART").defineArray(range(1,5));// WF-Dokument(für OD)
@L2 P xUMS_KEYCOUNTER = P(5,0 ,"#UMS-KEYCOUNTER").defineArray(range(1,10));// Identcounter aus File111
@L2 A xUMS_SKT_BELEGTYP = A(1 ,"#UMS-SKT-BELEGTYP");// Schaden-Konto-Belegtyp
@L2 P xUMS_SKT_POS_IDC = P(5,0 ,"#UMS-SKT-POS-IDC");// Identcounter Pos.Buchung-SKT
@L2 P xUMS_SKT_BRAN_IDC = P(5,0 ,"#UMS-SKT-BRAN-IDC");// Identcounter Bran.Buchung-SKT
@L2 P xUMS_SKT_ABST_IDC = P(5,0 ,"#UMS-SKT-ABST-IDC");// Identcounter Abst.Buchung-SKT
@L2 A xUMS_TEXT_FN95 = A(1 ,"#UMS-TEXT-FN95");// Text-Option im FN95
@L2 A xUMS_REGRESS_RATE = A(2 ,"#UMS-REGRESS-RATE");// Schaden-Regreß oder -Rate
@L2 P xUMS_REGRESS_NR = P(6,0 ,"#UMS-REGRESS-NR");// Schaden-Regreß-Nr.
@L2 A xUMS_DAT2_FN95 = A(1 ,"#UMS-DAT2-FN95");// 2. Datumszeile FN95 optional
@L2 N xUMS_PC_DAT = N(8,0 ,"#UMS-PC-DAT"); // Pc-Antragsdatum
@L2 N xUMS_PC_NUM = N(4,0 ,"#UMS-PC-NUM"); // Pc-Antragsnummer
@L2 A xUMS_DRUCKER_ID = A(10 ,"#UMS-DRUCKER-ID");// Drucker-Id (OD)
@L2 A xUMS_KEY_STAT_DI32 = A(8 ,"#UMS-KEY-STAT-DI32");// Station für AufrufDI32 (OD)
@L2 B xUMS_SKT_ANZEIGEDAT_KPL = B(3 ,"#UMS-SKT-ANZEIGEDAT-KPL");// Anzeigedatum-KPL für Sammelb.
@L2 P xUMS_WINDOW_NUM = P(2,0 ,"#UMS-WINDOW-NUM");// Image-Display (OD)
@L2 P xUMS_STORNO_GRUND = P(2,0 ,"#UMS-STORNO-GRUND");// stornogrund (nur LV)
@L2 A xUMS_DOI_KEY_ART = A(2 ,"#UMS-DOI-KEY-ART");// Kz f. Einschränkung Di30 (OD)
@L2 P xUMS_SKT_WERTEDATUM = P(8,0 ,"#UMS-SKT-WERTEDATUM");// Schaden-Konto-Wertedatum
@L2 P xUMS_SKT_BANKSCHLUESSEL = P(2,0 ,"#UMS-SKT-BANKSCHLUESSEL");// Schaden-Konto-Bankschlüssel
@L2 A xUMS_REST = A(28 ,"#UMS-REST"); // Bytes frei
@L1 A gPEL = A(250 ,"+PEL");
@L1 Redefine gPEL; // REDEF. BEGIN : +PEL
@L2 A xRA_PEL_ABT = A(1 ,"#RA-PEL-ABT");
@L2 N xRN_PEL_VPOL = N(8,0 ,"#RN-PEL-VPOL");
@L2 N xRN_PEL_KPNR = N(8,0 ,"#RN-PEL-KPNR");
@L2 N xRN_PEL_GT = N(8,0 ,"#RN-PEL-GT");
@L2 N xRN_PEL_BR1 = N(8,0 ,"#RN-PEL-BR1");
@L2 N xRN_PEL_BR2 = N(8,0 ,"#RN-PEL-BR2");
@L2 N xRN_PEL_BUCH = N(8,0 ,"#RN-PEL-BUCH");
@L2 N xRN_PEL_EINL = N(6,0 ,"#RN-PEL-EINL");
@L2 A xRA_PEL_FREI = A(2 ,"#RA-PEL-FREI");
@L2 N xRN_PEL_TEXT = N(8,0 ,"#RN-PEL-TEXT");
@L2 N xRN_PEL_STAT = N(8,0 ,"#RN-PEL-STAT");
@L2 N xRN_PEL_AGT = N(8,0 ,"#RN-PEL-AGT");
@L2 N xRN_PEL_ABR1 = N(8,0 ,"#RN-PEL-ABR1");
@L2 N xRN_PEL_ABR2 = N(8,0 ,"#RN-PEL-ABR2");
@L2 N xRN_PEL_ATXT = N(8,0 ,"#RN-PEL-ATXT");
@L2 N xRN_PEL_INDX = N(2,0 ,"#RN-PEL-INDX");
@L2 N xRN_PEL_VDAT = N(6,0 ,"#RN-PEL-VDAT");
@L2 A xRA_PEL_EFKZ = A(1 ,"#RA-PEL-EFKZ");
@L2 A xRA_PEL_DRUC = A(4 ,"#RA-PEL-DRUC");
@L2 A xRA_PEL_VART = A(3 ,"#RA-PEL-VART");
@L2 N xRN_PEL_KREI = N(3,0 ,"#RN-PEL-KREI");
@L2 A xRA_PEL_VAE = A(3 ,"#RA-PEL-VAE");
@L2 A xRA_PEL_BNEU = A(3 ,"#RA-PEL-BNEU");
@L2 A xRA_PEL_FILL118 = A(118 ,"#RA-PEL-FILL118");
@L2 A xRA_PEL_MENU = A(2 ,"#RA-PEL-MENU");
@L1 Redefine gPEL; // REDEF. BEGIN : +PEL
@L2 N xRN_KFZ_MENR = N(1,0 ,"#RN-KFZ-MENR");
@L2 N xRN_KFZ_IEVI = N(3,0 ,"#RN-KFZ-IEVI");
@L2 N xRN_KFZ_IVV = N(3,0 ,"#RN-KFZ-IVV");
@L2 N xRN_KFZ_HIST = N(3,0 ,"#RN-KFZ-HIST");
@L2 N xRN_KFZ_STND = N(3,0 ,"#RN-KFZ-STND");
@L2 A xRA_KFZ_EVBR = A(30 ,"#RA-KFZ-EVBR");
@L2 Redefine xRA_KFZ_EVBR;
@L3 A xRA_KFZ_BRA = A(3 ,"#RA-KFZ-BRA").defineArray(range(1, 10));
@L2 Redefine xRA_KFZ_EVBR;
@L3 N xRN_KFZ_BRA1 = N(3,0 ,"#RN-KFZ-BRA1");
@L3 N xRN_KFZ_BRA2 = N(3,0 ,"#RN-KFZ-BRA2");
@L3 N xRN_KFZ_BRA3 = N(3,0 ,"#RN-KFZ-BRA3");
@L3 N xRN_KFZ_BRA4 = N(3,0 ,"#RN-KFZ-BRA4");
@L3 N xRN_KFZ_BRA5 = N(3,0 ,"#RN-KFZ-BRA5");
@L2 N xRN_KFZ_VDAT = N(8,0 ,"#RN-KFZ-VDAT");
@L2 A xRA_KFZ_AREV = A(1 ,"#RA-KFZ-AREV");
@L2 A xRA_KFZ_SABE = A(3 ,"#RA-KFZ-SABE");
@L2 A xRA_KFZ_RAG = A(1 ,"#RA-KFZ-RAG");
@L2 P xRN_KFZ_OZW = P(3,0 ,"#RN-KFZ-OZW");
@L2 N xRN_KFZ_VVAE = N(8,0 ,"#RN-KFZ-VVAE");
@L2 A xRA_KFZ_VEF = A(1 ,"#RA-KFZ-VEF");
@L2 A xRA_KFZ_VVAR = A(3 ,"#RA-KFZ-VVAR");
@L2 N xRN_KFZ_VEGD = N(8,0 ,"#RN-KFZ-VEGD");
@L2 N xRN_KFZ_VELD = N(8,0 ,"#RN-KFZ-VELD");
@L2 A xRA_KFZ_VDRU = A(4 ,"#RA-KFZ-VDRU");
@L2 A xRA_KFZ_PZSG = A(3 ,"#RA-KFZ-PZSG");
@L2 N xRN_KFZ_LESE = N(3,0 ,"#RN-KFZ-LESE");
@L2 N xRN_KFZ_FZW = N(2,0 ,"#RN-KFZ-FZW");
@L2 N xRN_KFZ_VI10 = N(2,0 ,"#RN-KFZ-VI10");
@L2 N xRN_KFZ_VD = N(8,0 ,"#RN-KFZ-VD");
@L2 N xRN_KFZ_VB = N(8,0 ,"#RN-KFZ-VB");
@L2 A xRA_KFZ_KFKZ = A(11 ,"#RA-KFZ-KFKZ");
@L2 N xRN_KFZ_AWDT = N(8,0 ,"#RN-KFZ-AWDT");
@L2 A xRA_KFZ_AWKZ = A(1 ,"#RA-KFZ-AWKZ");
@L2 N xRN_KFZ_AEND = N(8,0 ,"#RN-KFZ-AEND");
@L2 A xRA_KFZ_VABT = A(3 ,"#RA-KFZ-VABT");
@L2 A xRA_KFZ_OPID = A(3 ,"#RA-KFZ-OPID");
@L2 N xRN_KFZ_DATV = N(8,0 ,"#RN-KFZ-DATV");
@L2 N xRN_KFZ_DATB = N(8,0 ,"#RN-KFZ-DATB");
@L2 A xRA_KFZ_FIL = A(2 ,"#RA-KFZ-FIL");
@L2 N xRN_KFZ_KRBR = N(4,0 ,"#RN-KFZ-KRBR");
@L2 Redefine xRN_KFZ_KRBR; // REDEF. BEGIN : #RN-KFZ-KRBR
@L3 N xRN_KFZ_KR = N(2,0 ,"#RN-KFZ-KR");
@L3 N xRN_KFZ_BR = N(2,0 ,"#RN-KFZ-BR");
@L2 A xRA_KFZ_VAE1 = A(3 ,"#RA-KFZ-VAE1");
@L2 A xRA_KFZ_VAE2 = A(3 ,"#RA-KFZ-VAE2");
@L2 A xRA_KFZ_SUMM = A(1 ,"#RA-KFZ-SUMM");
@L2 N xRN_KFZ_STAT = N(2,0 ,"#RN-KFZ-STAT");
@L2 A xRA_KFZ_GSNA = A(10 ,"#RA-KFZ-GSNA");
@L2 A xRA_KFZ_BVNR = A(7 ,"#RA-KFZ-BVNR");
@L2 N xRN_KFZ_ANTR = N(8,0 ,"#RN-KFZ-ANTR");
@L2 N xRN_KFZ_VINK = N(8,0 ,"#RN-KFZ-VINK");
@L2 N xRN_KFZ_FPNR = N(3,0 ,"#RN-KFZ-FPNR");
@L2 A xRA_KFZ_MAPA = A(1 ,"#RA-KFZ-MAPA");
@L2 L xRL_KFZ_PRVV = L("#RL-KFZ-PRVV");
@L2 L xRL_KFZ_ST01 = L("#RL-KFZ-ST01");
@L2 L xRL_KFZ_AUTO = L("#RL-KFZ-AUTO");
@L2 A xRA_KFZ_OPI2 = A(3 ,"#RA-KFZ-OPI2");
@L2 L xRL_KFZ_BLND = L("#RL-KFZ-BLND");
@L2 C xRC_KFZ_BLND = C("#RC-KFZ-BLND");
@L2 A xRA_KFZ_ATXT = A(10 ,"#RA-KFZ-ATXT");
@L2 A xRA_KFZ_KTXT = A(3 ,"#RA-KFZ-KTXT");
@L2 A xRA_KFZ_AMNU = A(8 ,"#RA-KFZ-AMNU");
@L2 A xRA_KFZ_MENU = A(2 ,"#RA-KFZ-MENU");
@L1 A gISN = A(250 ,"+ISN");
@L1 Redefine gISN; // REDEF. BEGIN : +ISN
@L2 A xRA_ISN_ISN = A(5 ,"#RA-ISN-ISN");
@L2 Redefine xRA_ISN_ISN; // REDEF. BEGIN : #RA-ISN-ISN
@L3 P xRN_ISN_AKUN = P(9,0 ,"#RN-ISN-AKUN");
@L2 P xRN_ISN_HKUN = P(9,0 ,"#RN-ISN-HKUN");
@L2 P xRN_ISN_VKUN = P(9,0 ,"#RN-ISN-VKUN");
@L2 P xRN_ISN_ORTE = P(9,0 ,"#RN-ISN-ORTE");
@L2 P xRN_ISN_KORP = P(9,0 ,"#RN-ISN-KORP");
@L2 P xRN_ISN_KTAB = P(9,0 ,"#RN-ISN-KTAB");
@L2 P xRN_ISN_ABKU = P(9,0 ,"#RN-ISN-ABKU");
@L2 P xRN_ISN_OPOL = P(9,0 ,"#RN-ISN-OPOL");
@L2 P xRN_ISN_SPOL = P(9,0 ,"#RN-ISN-SPOL");
@L2 P xRN_ISN_OEVI = P(9,0 ,"#RN-ISN-OEVI");
@L2 P xRN_ISN_SEVI = P(9,0 ,"#RN-ISN-SEVI");
@L2 P xRN_ISN_OVV = P(9,0 ,"#RN-ISN-OVV");
@L2 P xRN_ISN_SVV = P(9,0 ,"#RN-ISN-SVV");
@L2 P xRN_ISN_OBU = P(9,0 ,"#RN-ISN-OBU");
@L2 P xRN_ISN_SBU = P(9,0 ,"#RN-ISN-SBU");
@L2 P xRN_ISN_BTS = P(9,0 ,"#RN-ISN-BTS");
@L2 P xRN_ISN_BTK = P(9,0 ,"#RN-ISN-BTK");
@L2 P xRN_ISN_EKPO = P(9,0 ,"#RN-ISN-EKPO");
@L2 P xRN_ISN_EKVV = P(9,0 ,"#RN-ISN-EKVV");
@L2 P xRN_ISN_EKK1 = P(9,0 ,"#RN-ISN-EKK1");
@L2 P xRN_ISN_OPOS = P(9,0 ,"#RN-ISN-OPOS");
@L2 P xRN_ISN_SPOS = P(9,0 ,"#RN-ISN-SPOS");
@L2 P xRN_ISN_EKG1 = P(9,0 ,"#RN-ISN-EKG1");
@L2 P xRN_ISN_HGPO = P(9,0 ,"#RN-ISN-HGPO");
@L2 P xRN_ISN_HGKO = P(9,0 ,"#RN-ISN-HGKO");
@L2 P xRN_ISN_OLS = P(9,0 ,"#RN-ISN-OLS");
@L2 P xRN_ISN_SLS = P(9,0 ,"#RN-ISN-SLS");
@L2 P xRN_ISN_HLS = P(9,0 ,"#RN-ISN-HLS");
@L2 P xRN_ISN_VDNR = P(9,0 ,"#RN-ISN-VDNR");
@L2 P xRN_ISN_MDST = P(9,0 ,"#RN-ISN-MDST");
@L2 P xRN_ISN_KTOS = P(9,0 ,"#RN-ISN-KTOS");
@L2 P xRN_ISN_KTOO = P(9,0 ,"#RN-ISN-KTOO");
@L2 P xRN_ISN_ORTS = P(9,0 ,"#RN-ISN-ORTS");
@L2 P xRN_ISN_FPNR = P(9,0 ,"#RN-ISN-FPNR");
@L2 P xRN_ISN_WA49 = P(9,0 ,"#RN-ISN-WA49");
@L2 P xRN_ISN_SD34 = P(8,0 ,"#RN-ISN-SD34");
@L2 P xRN_ISN_SPER = P(9,0 ,"#RN-ISN-SPER");
@L2 P xRN_ISN_MDS = P(9,0 ,"#RN-ISN-MDS");
@L2 P xRN_ISN_SUCH = P(9,0 ,"#RN-ISN-SUCH");
@L2 P xRN_ISN_ABST = P(9,0 ,"#RN-ISN-ABST");
@L2 P xRN_ISN_UMKON = P(9,0 ,"#RN-ISN-UMKON");
@L2 P xRN_ISN_MUBU = P(9,0 ,"#RN-ISN-MUBU");// MUTTERBUCHUNG (SK)
@L2 P xRN_ISN_RESERVE = P(9,0 ,"#RN-ISN-RESERVE").defineArray(range(1, 5));
@L1 A gKUN = A(250 ,"+KUN");
@L1 Redefine gKUN; // REDEF. BEGIN : +KUN
@L2 A xRA_KUN_KUNA = A(27 ,"#RA-KUN-KUNA");
@L2 A xRA_KUN_GHKZ = A(1 ,"#RA-KUN-GHKZ");
@L2 N xRN_KUN_GHK1 = N(8,0 ,"#RN-KUN-GHK1");
@L2 N xRN_KUN_GHK2 = N(8,0 ,"#RN-KUN-GHK2");
@L2 N xRN_KUN_DATV = N(8,0 ,"#RN-KUN-DATV");
@L2 N xRN_KUN_DATB = N(8,0 ,"#RN-KUN-DATB");
@L2 A xRA_KUN_BRAX = A(30 ,"#RA-KUN-BRAX");
@L2 N xRN_KUN_FILV = N(2,0 ,"#RN-KUN-FILV");
@L2 N xRN_KUN_FILB = N(2,0 ,"#RN-KUN-FILB");
@L2 N xRN_KUN_ANZEI = N(2,0 ,"#RN-KUN-ANZEI");
@L2 N xRN_KUN_BILKR = N(2,0 ,"#RN-KUN-BILKR");
@L2 N xRN_KUN_POLK = N(2,0 ,"#RN-KUN-POLK");
@L2 N xRN_KUN_POLB = N(2,0 ,"#RN-KUN-POLB");
@L2 A xRA_KUN_BANKKU = A(5 ,"#RA-KUN-BANKKU");
@L2 A xRA_KUN_BANKOR = A(27 ,"#RA-KUN-BANKOR");
@L2 N xRN_KUN_BANKLE = N(5,0 ,"#RN-KUN-BANKLE");
@L2 N xRN_KUN_TDAT = N(8,0 ,"#RN-KUN-TDAT");
@L2 N xRN_KUN_VR = N(3,0 ,"#RN-KUN-VR");
@L2 N xRN_KUN_VGVR = N(3,0 ,"#RN-KUN-VGVR");
@L2 N xRN_KUN_STAT = N(3,0 ,"#RN-KUN-STAT");
@L2 N xRN_KUN_HGV = N(2,0 ,"#RN-KUN-HGV");
@L2 A xRA_KUN_UG1V = A(2 ,"#RA-KUN-UG1V");
@L2 A xRA_KUN_UG2V = A(2 ,"#RA-KUN-UG2V");
@L2 A xRA_KUN_UG3V = A(2 ,"#RA-KUN-UG3V");
@L2 A xRA_KUN_SPV = A(1 ,"#RA-KUN-SPV");
@L2 N xRN_KUN_HGB = N(2,0 ,"#RN-KUN-HGB");
@L2 A xRA_KUN_UG1B = A(2 ,"#RA-KUN-UG1B");
@L2 A xRA_KUN_UG2B = A(2 ,"#RA-KUN-UG2B");
@L2 A xRA_KUN_UG3B = A(2 ,"#RA-KUN-UG3B");
@L2 A xRA_KUN_SPB = A(1 ,"#RA-KUN-SPB");
@L2 A xRA_KUN_LEER1 = A(38 ,"#RA-KUN-LEER1");
@L2 N xRN_KUN_OVGEM = N(5,0 ,"#RN-KUN-OVGEM");
@L2 N xRN_KUN_OVOKZ = N(6,0 ,"#RN-KUN-OVOKZ");
@L2 N xRN_KUN_OVOBK = N(4,0 ,"#RN-KUN-OVOBK");
@L2 A xRA_KUN_OVNAM = A(15 ,"#RA-KUN-OVNAM");
@L2 A xRA_KUN_LEER2 = A(4 ,"#RA-KUN-LEER2");
@L2 A xRA_KUN_OVFIL = A(2 ,"#RA-KUN-OVFIL");
@L2 A xRA_KUN_OVSA = A(1 ,"#RA-KUN-OVSA");
@L1 A gKEY = A(250 ,"+KEY");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 N xRN_KEY_VKKZ = N(8,0 ,"#RN-KEY-VKKZ");
@L2 A xRA_KEY_VVA = A(2 ,"#RA-KEY-VVA");
@L2 N xRN_KEY_VVNR = N(8,0 ,"#RN-KEY-VVNR");
@L2 N xRN_KEY_VHNR = N(3,0 ,"#RN-KEY-VHNR");
@L2 N xRN_KEY_VGES = N(3,0 ,"#RN-KEY-VGES");
@L2 N xRN_KEY_VRES = N(1,0 ,"#RN-KEY-VRES");
@L2 N xRN_KEY_VVIN = N(2,0 ,"#RN-KEY-VVIN");
@L2 N xRN_KEY_VBRA = N(2,0 ,"#RN-KEY-VBRA");
@L2 N xRN_KEY_VBRN = N(1,0 ,"#RN-KEY-VBRN");
@L2 A xRA_KEY_VBTART = A(1 ,"#RA-KEY-VBTART");
@L2 A xRA_KEY_VBSTAT = A(1 ,"#RA-KEY-VBSTAT");
@L2 N xRN_KEY_VUGES = N(3,0 ,"#RN-KEY-VUGES");
@L2 N xRN_KEY_VBJAHR = N(2,0 ,"#RN-KEY-VBJAHR");
@L2 N xRN_KEY_AKTONR = N(7,0 ,"#RN-KEY-AKTONR");
@L2 Redefine xRN_KEY_AKTONR; // REDEF. BEGIN : #RN-KEY-AKTONR
@L3 A xRA_KEY_VKTO = A(7 ,"#RA-KEY-VKTO");
@L2 A xRA_KEY_KOSEL = A(1 ,"#RA-KEY-KOSEL");
@L2 A xRA_KEY_LEER1 = A(4 ,"#RA-KEY-LEER1");
@L2 N xRN_KEY_AKKZ = N(8,0 ,"#RN-KEY-AKKZ");
@L2 A xRA_KEY_AVA = A(2 ,"#RA-KEY-AVA");
@L2 N xRN_KEY_AVNR = N(8,0 ,"#RN-KEY-AVNR");
@L2 N xRN_KEY_AHNR = N(3,0 ,"#RN-KEY-AHNR");
@L2 N xRN_KEY_AGES = N(3,0 ,"#RN-KEY-AGES");
@L2 N xRN_KEY_ARES = N(1,0 ,"#RN-KEY-ARES");
@L2 N xRN_KEY_AVIN = N(2,0 ,"#RN-KEY-AVIN");
@L2 N xRN_KEY_ABRA = N(2,0 ,"#RN-KEY-ABRA");
@L2 N xRN_KEY_ABRN = N(1,0 ,"#RN-KEY-ABRN");
@L2 A xRA_KEY_ABTART = A(1 ,"#RA-KEY-ABTART");
@L2 A xRA_KEY_ABSTAT = A(1 ,"#RA-KEY-ABSTAT");
@L2 N xRN_KEY_AUGES = N(3,0 ,"#RN-KEY-AUGES");
@L2 N xRN_KEY_ABJAHR = N(2,0 ,"#RN-KEY-ABJAHR");
@L2 N xRN_KEY_KTONR = N(7,0 ,"#RN-KEY-KTONR");
@L2 N xRN_KEY_PROV = N(2,0 ,"#RN-KEY-PROV");
@L2 N xRN_KEY_PROD = N(2,0 ,"#RN-KEY-PROD");
@L2 A xRA_KEY_PRKZ = A(1 ,"#RA-KEY-PRKZ");
@L2 N xRN_KEY_AFGS = N(3,0 ,"#RN-KEY-AFGS");
@L2 A xRA_KEY_AFPO = A(14 ,"#RA-KEY-AFPO");
@L2 N xRN_KEY_VFGS = N(3,0 ,"#RN-KEY-VFGS");
@L2 A xRA_KEY_VFPO = A(14 ,"#RA-KEY-VFPO");
@L2 N xRN_KEY_FBRA = N(2,0 ,"#RN-KEY-FBRA");
@L2 N xRN_KEY_NOTD = N(6,0 ,"#RN-KEY-NOTD");
@L2 Redefine xRN_KEY_NOTD;
@L3 N xRN_KEY_NOTD_YYYY = N(4,0 ,"#RN-KEY-NOTD-YYYY");
@L3 N xRN_KEY_NOTD_MM = N(2,0 ,"#RN-KEY-NOTD-MM");
@L2 N xRN_KEY_LSNR = N(2,0 ,"#RN-KEY-LSNR");
@L2 N xRN_KEY_KBJ = N(4,0 ,"#RN-KEY-KBJ");
@L2 N xRN_KEY_KBJNR = N(1,0 ,"#RN-KEY-KBJNR");
@L2 A xRA_KEY_ZNAM = A(31 ,"#RA-KEY-ZNAM");
@L2 A xRA_KEY_VNAM = A(2 ,"#RA-KEY-VNAM");
@L2 A xRA_KEY_STRA = A(1 ,"#RA-KEY-STRA");
@L2 N xRN_KEY_HAUS = N(4,0 ,"#RN-KEY-HAUS");
@L2 N xRN_KEY_POST = N(6,0 ,"#RN-KEY-POST");
@L2 A xRA_KEY_KZEX = A(1 ,"#RA-KEY-KZEX");
@L2 A xRA_KEY_ACHT = A(1 ,"#RA-KEY-ACHT");
@L2 N xRN_KEY_ALFA = N(2,0 ,"#RN-KEY-ALFA");
@L2 N xRN_KEY_BERU = N(5,0 ,"#RN-KEY-BERU");
@L2 A xRA_KEY_LEER2 = A(4 ,"#RA-KEY-LEER2");
@L2 A xRA_KEY_BRSL = A(1 ,"#RA-KEY-BRSL");
@L2 A xRA_KEY_PSTA = A(1 ,"#RA-KEY-PSTA");
@L2 A xRA_KEY_BRAW = A(1 ,"#RA-KEY-BRAW");
@L2 N xRN_KEY_BRVO = N(2,0 ,"#RN-KEY-BRVO");
@L2 N xRN_KEY_BRBI = N(2,0 ,"#RN-KEY-BRBI");
@L2 N xRN_KEY_JRVO = N(2,0 ,"#RN-KEY-JRVO");
@L2 N xRN_KEY_JRBI = N(2,0 ,"#RN-KEY-JRBI");
@L2 A xRA_KEY_VNAW = A(1 ,"#RA-KEY-VNAW");
@L2 A xRA_KEY_KEIG = A(1 ,"#RA-KEY-KEIG");
@L2 A xRA_KEY_SBID = A(3 ,"#RA-KEY-SBID");
@L2 A xRA_KEY_KURO = A(2 ,"#RA-KEY-KURO");
@L2 A xRA_KEY_PRAE = A(1 ,"#RA-KEY-PRAE");
@L2 A xRA_KEY_PROV = A(1 ,"#RA-KEY-PROV");
@L2 A xRA_KEY_OFFE = A(1 ,"#RA-KEY-OFFE");
@L2 A xRA_KEY_AKTU = A(1 ,"#RA-KEY-AKTU");
@L2 A xRA_KEY_ALLE = A(1 ,"#RA-KEY-ALLE");
@L2 A xRA_KEY_AOFA = A(1 ,"#RA-KEY-AOFA");
@L2 N xRN_KEY_BUDT = N(8,0 ,"#RN-KEY-BUDT");
@L2 P xRN_KEY_BUNR = P(9,0 ,"#RN-KEY-BUNR");
@L2 A xRA_KEY_VKZ = A(1 ,"#RA-KEY-VKZ");
@L2 A xRA_KEY_ABGA = A(1 ,"#RA-KEY-ABGA");
@L2 A xRA_KEY_SART = A(2 ,"#RA-KEY-SART");
@L2 A xRA_KEY_PFIL = A(2 ,"#RA-KEY-PFIL");
@L2 L xRL_KEY_UHR = L("#RL-KEY-UHR");
@L2 A xRA_KEY_STAT = A(1 ,"#RA-KEY-STAT");
@L2 A xRA_KEY_GSMT = A(1 ,"#RA-KEY-GSMT");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_1 = A(8 ,"#RA-KEY-08X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_VVO = A(22 ,"#RA-KEY-VVO");
@L2 A xRA_KEY_27X_1 = A(27 ,"#RA-KEY-27X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_AVO = A(22 ,"#RA-KEY-AVO");
@L2 A xRA_KEY_27X_2 = A(27 ,"#RA-KEY-27X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_GVO = A(22 ,"#RA-KEY-GVO");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_2 = A(8 ,"#RA-KEY-08X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_VBT = A(24 ,"#RA-KEY-VBT");
@L2 A xRA_KEY_24X = A(24 ,"#RA-KEY-24X"); // ZWISCHENRAUM
@L2 A xRA_KEY_ABT = A(24 ,"#RA-KEY-ABT");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_3 = A(8 ,"#RA-KEY-08X-3");// ZWISCHENRAUM
@L2 A xRA_KEY_VVK = A(16 ,"#RA-KEY-VVK");
@L2 A xRA_KEY_33X_1 = A(33 ,"#RA-KEY-33X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_AVK = A(16 ,"#RA-KEY-AVK");
@L2 A xRA_KEY_33X_2 = A(33 ,"#RA-KEY-33X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_GVK = A(16 ,"#RA-KEY-GVK");
@L1 A gSTV = A(250 ,"+STV");
@L1 Redefine gSTV; // REDEF. BEGIN : +STV
@L2 A xRA_STV_APRG = A(8 ,"#RA-STV-APRG");
@L2 A xRA_STV_VPRG = A(8 ,"#RA-STV-VPRG");
@L2 A xRA_STV_FUNK = A(4 ,"#RA-STV-FUNK");
@L2 Redefine xRA_STV_FUNK; // REDEF. BEGIN : #RA-STV-FUNK
@L3 N xRN_STV_FUNR = N(2,0 ,"#RN-STV-FUNR");
@L3 A xRA_STV_FUNR = A(2 ,"#RA-STV-FUNR");
@L2 N xRN_STV_AUSW = N(2,0 ,"#RN-STV-AUSW");
@L2 A xRA_STV_CODE = A(4 ,"#RA-STV-CODE"); // 30
@L2 N xRN_STV_OBNR = N(8,0 ,"#RN-STV-OBNR");
@L2 A xRA_STV_OART = A(2 ,"#RA-STV-OART");
@L2 N xRN_STV_HINR = N(3,0 ,"#RN-STV-HINR");
@L2 N xRN_STV_VINR = N(2,0 ,"#RN-STV-VINR");
@L2 N xRN_STV_BRAN = N(2,0 ,"#RN-STV-BRAN");
@L2 N xRN_STV_BRNR = N(1,0 ,"#RN-STV-BRNR");
@L2 N xRN_STV_GENR = N(3,0 ,"#RN-STV-GENR");
@L2 A xRA_STV_GMOD = A(1 ,"#RA-STV-GMOD");
@L2 A xRA_STV_AMOD = A(1 ,"#RA-STV-AMOD");
@L2 A xRA_STV_VIKZ = A(1 ,"#RA-STV-VIKZ");
@L2 A xRA_STV_DATI = A(8 ,"#RA-STV-DATI");
@L2 N xRN_STV_DATA = N(3,0 ,"#RN-STV-DATA");
@L2 A xRA_STV_OPID = A(3 ,"#RA-STV-OPID");
@L2 A xRA_STV_PFKY = A(96 ,"#RA-STV-PFKY");
@L2 Redefine xRA_STV_PFKY; // REDEF. BEGIN : #RA-STV-PFKY
@L3 A xRA_STV_PFF = A(4 ,"#RA-STV-PFF");
@L2 Redefine xRA_STV_PFKY; // REDEF. BEGIN : #RA-STV-PFKY
@L3 A xRA_STV_PF = A(4 ,"#RA-STV-PF").defineArray(range(1, 24));
@L2 N xRN_STV_DATN = N(6,0 ,"#RN-STV-DATN");
@L2 A xRA_STV_MMOD = A(3 ,"#RA-STV-MMOD");
@L1 Redefine gSTV; // REDEF. BEGIN : +STV
@L2 A xRA_STV_241X = A(241 ,"#RA-STV-241X");
@L2 A xRA_STV_APAR = A(1 ,"#RA-STV-APAR");
@L2 P xRN_STV_CURS = P(6,0 ,"#RN-STV-CURS");
@L2 P xRN_STV_MARK = P(3,0 ,"#RN-STV-MARK");
@L2 B xRN_STV_FNUM = B(2 ,"#RN-STV-FNUM");
@L1 A gKEY1 = A(250 ,"+KEY1");
@L1 Redefine gKEY1; // REDEF. BEGIN : +KEY1
@L2 A xRA_KEY1_ASSTART = A(4 ,"#RA-KEY1-ASSTART");
@L2 N xRN_KEY1_POSVI = N(2,0 ,"#RN-KEY1-POSVI");
@L2 N xRN_KEY1_POSVII = N(3,0 ,"#RN-KEY1-POSVII");
@L2 A xRA_KEY1_RVART = A(4 ,"#RA-KEY1-RVART");
@L2 N xRN_KEY1_RVVON = N(8,0 ,"#RN-KEY1-RVVON");
@L2 N xRN_KEY1_RVBHJAHR = N(4,0 ,"#RN-KEY1-RVBHJAHR");
@L2 N xRN_KEY1_RVBBNR = N(2,0 ,"#RN-KEY1-RVBBNR");
@L2 P xRN_KEY1_SBVANZ = P(1,0 ,"#RN-KEY1-SBVANZ");
@L2 P xRN_KEY1_SBVINR = P(2,0 ,"#RN-KEY1-SBVINR").defineArray(range(1, 2));
@L2 P xRN_KEY1_SBVII = P(2,0 ,"#RN-KEY1-SBVII").defineArray(range(1, 2));
@L2 N xRN_KEY1_BRA = N(2,0 ,"#RN-KEY1-BRA").defineArray(range(1, 3));
@L2 N xRN_KEY1_GEBDT = N(8,0 ,"#RN-KEY1-GEBDT");
@L2 A xRA_KEY1_2X = A(2 ,"#RA-KEY1-2X");
@L2 A xRA_KEY1_WAEHRUNG = A(3 ,"#RA-KEY1-WAEHRUNG");
@L2 A xRA_KEY1_SK_ALLE = A(1 ,"#RA-KEY1-SK-ALLE");
@L2 N xRN_KEY1_HNR = N(4,0 ,"#RN-KEY1-HNR");
@L2 A xRA_KEY1_MTVKZ = A(1 ,"#RA-KEY1-MTVKZ");
@L2 A xRA_KEY1_VIL = A(1 ,"#RA-KEY1-VIL");
@L2 A xRA_KEY1_OKZ = A(3 ,"#RA-KEY1-OKZ");
@L2 N xRN_KEY1_OPLZ = N(6,0 ,"#RN-KEY1-OPLZ");
@L2 N xRN_KEY1_OHNR = N(4,0 ,"#RN-KEY1-OHNR");
@L2 P xRN_KEY1_SUBVR = P(3,0 ,"#RN-KEY1-SUBVR");
@L2 P xRN_KEY1_SUBKTO = P(7,0 ,"#RN-KEY1-SUBKTO");
@L2 P xRN_KEY1_SUBZWST = P(3,0 ,"#RN-KEY1-SUBZWST");
@L2 A xRA_KEY1_SKART = A(3 ,"#RA-KEY1-SKART");
@L2 A xRA_KEY1_TCSART = A(1 ,"#RA-KEY1-TCSART");
@L2 A xRA_KEY1_TCZ1 = A(2 ,"#RA-KEY1-TCZ1");
@L2 Redefine xRA_KEY1_TCZ1; // REDEF. BEGIN : #RA-KEY1-TCZ1
@L3 N xRN_KEY1_TCZ1 = N(2,0 ,"#RN-KEY1-TCZ1");
@L2 P xRN_KEY1_TCODE = P(5,0 ,"#RN-KEY1-TCODE");
@L2 P xRN_KEY1_TCZ2 = P(2,0 ,"#RN-KEY1-TCZ2");
@L2 A xRA_KEY1_NATV = A(3 ,"#RA-KEY1-NATV");
@L2 A xRA_KEY1_NATO = A(3 ,"#RA-KEY1-NATO");
@L2 N xRN_KEY1_SNR = N(8,0 ,"#RN-KEY1-SNR");
@L2 N xRN_KEY1_LNR = N(5,0 ,"#RN-KEY1-LNR");
@L2 A xRA_KEY1_ZISB = A(3 ,"#RA-KEY1-ZISB");
@L2 N xRN_KEY1_STAT = N(5,0 ,"#RN-KEY1-STAT");
@L2 N xRN_KEY1_LZ = N(2,0 ,"#RN-KEY1-LZ");
@L2 A xRA_KEY1_GRDHA = A(2 ,"#RA-KEY1-GRDHA");
@L2 A xRA_KEY1_NATOV = A(3 ,"#RA-KEY1-NATOV");
@L2 N xRN_KEY1_AWDT = N(8,0 ,"#RN-KEY1-AWDT");
@L2 A xRA_KEY1_KOSP = A(1 ,"#RA-KEY1-KOSP");
@L2 N xRN_KEY1_EDSTAT = N(2,0 ,"#RN-KEY1-EDSTAT");
@L2 A xRA_KEY1_FREI = A(7 ,"#RA-KEY1-FREI");
@L2 N xRN_KEY1_KDAT = N(8,0 ,"#RN-KEY1-KDAT");
@L2 N xRN_KEY1_KGRD = N(2,0 ,"#RN-KEY1-KGRD");
@L2 N xRN_KEY1_PERSNR = N(5,0 ,"#RN-KEY1-PERSNR");
@L2 A xRA_KEY1_RPART = A(2 ,"#RA-KEY1-RPART");
@L2 N xRN_KEY1_RPDAT = N(8,0 ,"#RN-KEY1-RPDAT");
@L2 A xRA_KEY1_RVKZ = A(1 ,"#RA-KEY1-RVKZ");
@L2 A xRA_KEY1_RVKZ2 = A(1 ,"#RA-KEY1-RVKZ2");
@L2 A xRA_KEY1_LK = A(30 ,"#RA-KEY1-LK");
@L2 A xRA_KEY1_SCHEMA = A(2 ,"#RA-KEY1-SCHEMA");
@L2 N xRN_KEY1_RSOBNR = N(8,0 ,"#RN-KEY1-RSOBNR");
@L2 A xRA_KEY1_RSKZ = A(1 ,"#RA-KEY1-RSKZ");
@L2 A xRA_KEY1_BRFA = A(3 ,"#RA-KEY1-BRFA");
@L2 A xRA_KEY1_BUCHART = A(3 ,"#RA-KEY1-BUCHART");
@L2 N xRN_KEY1_BANKSCHL = N(3,0 ,"#RN-KEY1-BANKSCHL");
@L2 N xRN_KEY1_WERTEDAT = N(8,0 ,"#RN-KEY1-WERTEDAT");
@L2 A xRA_KEY1_VORPROG = A(8 ,"#RA-KEY1-VORPROG");
@L2 N xRN_KEY1_GILT_BIS = N(8,0 ,"#RN-KEY1-GILT-BIS");
@L2 P xRN_KEY1_FNR = P(3,0 ,"#RN-KEY1-FNR");
@L2 A xRA_KEY1_KO55MELD = A(1 ,"#RA-KEY1-KO55MELD");
@L1 A gKEY2 = A(250 ,"+KEY2");
@L1 Redefine gKEY2; // REDEF. BEGIN : +KEY2
@L2 A xRA_KEY2_TARIF = A(7 ,"#RA-KEY2-TARIF");
@L2 A xRA_KEY2_BANKNA = A(27 ,"#RA-KEY2-BANKNA");
@L2 A xRA_KEY2_USERGRUPPE = A(8 ,"#RA-KEY2-USERGRUPPE");
@L2 N xRN_KEY2_KOSTST = N(5,0 ,"#RN-KEY2-KOSTST");
@L2 A xRA_KEY2_AVISO_OP = A(3 ,"#RA-KEY2-AVISO-OP");
@L2 N xRN_KEY2_AVISO_DT = N(8,0 ,"#RN-KEY2-AVISO-DT");
@L2 N xRN_KEY2_AVISO_GD = N(3,0 ,"#RN-KEY2-AVISO-GD");
@L2 N xRN_KEY2_AVISO_DTB = N(8,0 ,"#RN-KEY2-AVISO-DTB");
@L2 A xRA_KEY2_SPRACH_KZ = A(2 ,"#RA-KEY2-SPRACH-KZ");
@L2 A xRA_KEY2_GEWINN_VB = A(4 ,"#RA-KEY2-GEWINN-VB");
@L2 N xRN_KEY2_GEWINN_JAHR = N(4,0 ,"#RN-KEY2-GEWINN-JAHR");
@L2 A xRA_KEY2_ABST_SA = A(2 ,"#RA-KEY2-ABST-SA");
@L2 A xRA_KEY2_ABST_ZA = A(1 ,"#RA-KEY2-ABST-ZA");
@L2 P xRN_KEY2_ABST_ZR = P(9,0 ,"#RN-KEY2-ABST-ZR");
@L2 A xRA_KEY2_ABST_NK = A(1 ,"#RA-KEY2-ABST-NK");
@L2 A xRA_KEY2_ABST_NA = A(8 ,"#RA-KEY2-ABST-NA");
@L2 A xRA_KEY2_FAB = A(3 ,"#RA-KEY2-FAB");
@L2 A xRA_KEY2_TYP = A(10 ,"#RA-KEY2-TYP");
@L2 A xRA_KEY2_FZA = A(1 ,"#RA-KEY2-FZA");
@L2 N xRN_KEY2_LST = N(1,0 ,"#RN-KEY2-LST");
@L2 N xRN_KEY2_KTBJ = N(4,0 ,"#RN-KEY2-KTBJ");
@L2 N xRN_KEY2_KW = N(3,0 ,"#RN-KEY2-KW");
@L2 P xRN_KEY2_UEBWNR = P(6,0 ,"#RN-KEY2-UEBWNR");
@L2 P xRN_KEY2_UNTNR = P(4,0 ,"#RN-KEY2-UNTNR");
@L2 A xRA_KEY2_SERIE = A(1 ,"#RA-KEY2-SERIE");
@L2 P xRN_KEY2_PAKNR = P(9,0 ,"#RN-KEY2-PAKNR");
@L2 A xRA_KEY2_WHG_VON = A(3 ,"#RA-KEY2-WHG-VON");
@L2 A xRA_KEY2_WHG_NACH = A(3 ,"#RA-KEY2-WHG-NACH");
@L2 A xRA_KEY2_EXTSA = A(3 ,"#RA-KEY2-EXTSA");
@L2 N xRN_KEY2_GKNR = N(8,0 ,"#RN-KEY2-GKNR");
@L1 C xCV_WHBS = C("#CV-WHBS"); // GW FUER WIEDERHOLTE BS-AUSG.
@L1 C xCV_OUTP = C("#CV-OUTP"); // PROTECT BEI A+B - PROGRAMMEN
@L1 C xCV_STV = C("#CV-STV"); // KEY-FELDER STEUERLEISTE
@L1 L xWL_KEYWECHS = L("#WL-KEYWECHS"); // SCHALTER, OB KEYWECHSEL
@L1 L xWL_INTERN = L("#WL-INTERN"); // SCHALTER, OB BLINDER DURCHL.
@L1 A xWA_HELP_TASTE = A(4 ,"#WA-HELP-TASTE"); // PF-TASTE FUER 'HELP'
@L1 A xWA_MENU_TASTE = A(4 ,"#WA-MENU-TASTE"); // PF-TASTE FUER 'MENU'
@L1 P xWN_SEL_ISN = P(9,0 ,"#WN-SEL-ISN").defineArray(range(1,24));
@L1 A xWA_FUNKTION = A(5 ,"#WA-FUNKTION").defineArray(range(1,100));// FUNKTIONSTABELLE
@L1 A xWA_VORWEG_AUFRUF = A(8 ,"#WA-VORWEG-AUFRUF");// VORWEG-AUFRUF (Z.B. SCHWEBE)
@L1 L xWL_TEST = L("#WL-TEST"); // KENNZEICHEN, OB TEST
@L1 N xWN_WARTEN_SUMME = N(5,0 ,"#WN-WARTEN-SUMME");// ZAEHLER FUER SUSPEND
@L1 A xWA_AUSWAHL_KZ = A(1 ,"#WA-AUSWAHL-KZ").defineArray(range(1,24));// MARKIERUNGSLEISTE
@L1 N xWN_AUSWAHL_ANZAHL = N(2,0 ,"#WN-AUSWAHL-ANZAHL");
@L1 A xWA_AUSWAHL_GUELTIG = A(20 ,"#WA-AUSWAHL-GUELTIG");
/*
* Platz für neue globale Variable
* Definitionen in:
* YLNEUGLO/YCNEUGLO
*/
@L1 A gNEUGLO_01 = A(250 ,"+NEUGLO-01");
@L1 Redefine gNEUGLO_01;
@L2 A xBTC_STEUERUNG = A(8 ,"#BTC-STEUERUNG");
@L2 A xUMS_MIS_BILANZKREIS = A(1 ,"#UMS-MIS-BILANZKREIS").defineArray(range(1, 7));
@L2 A xUMS_MIS_BRAN = A(2 ,"#UMS-MIS-BRAN").defineArray(range(1, 10));
@L2 P xKEY1_SUCH_BETRAG = P(9,2 ,"#KEY1-SUCH-BETRAG");
@L2 P xKEY2_ARCHIV_DB = P(3,0 ,"#KEY2-ARCHIV-DB");
@L2 A xKEY2_EXTKZ = A(1 ,"#KEY2-EXTKZ");
@L2 N xRN_KEY_JAHRV = N(4,0 ,"#RN-KEY-JAHRV");
@L2 N xRN_KEY_JAHRB = N(4,0 ,"#RN-KEY-JAHRB");
@L2 A xRA_KEY_EXT_GRUND_NAME = A(31 ,"#RA-KEY-EXT-GRUND-NAME");
@L2 P xRN_KEY_EXT_BB_VON = P(8,0 ,"#RN-KEY-EXT-BB-VON");
@L2 P xRN_KEY_EXT_BB_BIS = P(8,0 ,"#RN-KEY-EXT-BB-BIS");
@L2 P xUMS_MA_SUB_SATZART_1 = P(3,0 ,"#UMS-MA-SUB-SATZART-1");
@L2 A xRA_KEY_EXT_ANTRAGSART = A(3 ,"#RA-KEY-EXT-ANTRAGSART");
@L2 P xFLV_TRANS = P(7,0 ,"#FLV-TRANS");
@L2 A xFLV_ISIN = A(12 ,"#FLV-ISIN");
@L2 P xFLV_BUNR = P(7,0 ,"#FLV-BUNR");
@L2 P xFLV_GILT_BIS = P(8,0 ,"#FLV-GILT-BIS");
@L2 P xFLV_DATUM = P(8,0 ,"#FLV-DATUM");
@L2 P xFLV_VV_VINR = P(3,0 ,"#FLV-VV-VINR");
@L2 P xFLV_VV_BRAN = P(3,0 ,"#FLV-VV-BRAN");
@L2 P xFLV_VV_BRNR = P(3,0 ,"#FLV-VV-BRNR");
@L2 P xFLV_VV_HINR = P(3,0 ,"#FLV-VV-HINR");
@L2 A xVLK_EXVERARB = A(30 ,"#VLK-EXVERARB");
@L2 P xRN_KEY_IVKNR = P(8,0 ,"#RN-KEY-IVKNR");
@L2 A xRA_SC_MELDUNGS_ART = A(1 ,"#RA-SC-MELDUNGS-ART");
@L2 A xRA_OS_GFALL = A(2 ,"#RA-OS-GFALL");
@L2 A xRA_OS_DOKART = A(4 ,"#RA-OS-DOKART");
@L2 A xRA_OS_BR_ART = A(3 ,"#RA-OS-BR-ART");
@L2 N xRN_KEY_YU_PERSNR = N(13,0 ,"#RN-KEY-YU-PERSNR");
@L2 N xSCN_VERS_BEGINN = N(8,0 ,"#SCN-VERS-BEGINN");
@L2 A xDI_ABTEILUNG = A(8 ,"#DI-ABTEILUNG");
@L2 P xDI_STAT_DZT = P(3,0 ,"#DI-STAT-DZT");
@L2 P xDI_STAT_DZT_BIS = P(3,0 ,"#DI-STAT-DZT-BIS");
@L2 A xDI_SPARTE = A(2 ,"#DI-SPARTE");
@L2 A xDI_STATION = A(8 ,"#DI-STATION");
@L2 A xDI_WM_OPID = A(8 ,"#DI-WM-OPID");
@L2 A xDI_AW_POL_FIL = A(1 ,"#DI-AW-POL-FIL");
@L2 A xDI_VAE_ART = A(3 ,"#DI-VAE-ART");
@L2 A xDI_WK_VAE_ART = A(3 ,"#DI-WK-VAE-ART");
@L2 A xDI_AW_SUB_VR = A(1 ,"#DI-AW-SUB-VR");
@L2 P xDI_SUB_VR = P(3,0 ,"#DI-SUB-VR");
@L2 P xDI_POL_KREIS = P(2,0 ,"#DI-POL-KREIS");
@L2 P xDI_POL_BRAN = P(2,0 ,"#DI-POL-BRAN");
@L2 A xDI_VAE_HINW = A(3 ,"#DI-VAE-HINW");
@L1 A gNEUGLO_02 = A(250 ,"+NEUGLO-02");
@L1 Redefine gNEUGLO_02;
@L2 A xDI_BEARB_GRP = A(3 ,"#DI-BEARB-GRP");
@L2 A xDI_AW_DRF = A(1 ,"#DI-AW-DRF");
@L2 A xDI_AW_E_ANT = A(1 ,"#DI-AW-E-ANT");
@L2 P xDI_PC_ANT_VKTO = P(7,0 ,"#DI-PC-ANT-VKTO");
@L2 P xDI_PLZ_VON = P(7,0 ,"#DI-PLZ-VON");
@L2 P xDI_PLZ_BIS = P(7,0 ,"#DI-PLZ-BIS");
@L2 A xDI_POSTNATION = A(3 ,"#DI-POSTNATION");
@L2 P xDI_WM_DATUM = P(8,0 ,"#DI-WM-DATUM");
@L2 P xDI_POL_FILIALE = P(2,0 ,"#DI-POL-FILIALE");
@L2 A xDI_AW_VAE_HINW = A(1 ,"#DI-AW-VAE-HINW");
@L2 A xDI_AW_VKTO = A(1 ,"#DI-AW-VKTO");
@L2 A xDI_AW_BEARB_GRP = A(1 ,"#DI-AW-BEARB-GRP");
@L2 A xDI_AW_KR_BR = A(1 ,"#DI-AW-KR-BR");
@L2 A xDI_AW_VAE_ART = A(1 ,"#DI-AW-VAE-ART");
@L2 P xDI_VR_KEY = P(3,0 ,"#DI-VR-KEY");
@L2 A xDI_OART = A(2 ,"#DI-OART");
@L2 P xDI_WM_DATUM_VON = P(8,0 ,"#DI-WM-DATUM-VON");
@L2 A xDI_WM_AUSWAHL = A(1 ,"#DI-WM-AUSWAHL");
@L2 A xRA_KEY_OIB = A(20 ,"#RA-KEY-OIB");
}

View File

@@ -2,6 +2,8 @@ package com.agenticcode.neo4jstore.graph;
import com.agenticcode.parsercore.ast.model.NodeType;
import org.jspecify.annotations.Nullable;
import java.util.List;
/**
@@ -23,7 +25,21 @@ import java.util.List;
* (item J2). Graphs ingested before call-kind stamping fall back to the
* legacy {@code CALLNAT}/{@code PERFORM} labels derived from the target
* node type.
* @param project item 237: the project the row's module lives in when it is not the queried
* one — a caller in a client project reaching this module through
* {@code COUNTERPART_OF} (korin-ws skeleton → korin facade); {@code null} otherwise
* @param counterpartProject item 237: for an unresolved callee, the project of its {@code COUNTERPART_OF} twin
* @param counterpartModule item 237: that twin's module
*/
public record AggregatedCallRef(String name, NodeType type, int sourceFileIndex, String edgeKind,
List<CallSite> sites, boolean unresolved) {
List<CallSite> sites, boolean unresolved, @Nullable String project,
@Nullable String counterpartProject, @Nullable String counterpartModule) {
/**
* A row of the queried project without a counterpart.
*/
public AggregatedCallRef(String name, NodeType type, int sourceFileIndex, String edgeKind, List<CallSite> sites,
boolean unresolved) {
this(name, type, sourceFileIndex, edgeKind, sites, unresolved, null, null, null);
}
}

View File

@@ -1,5 +1,7 @@
package com.agenticcode.neo4jstore.graph;
import java.util.List;
import com.agenticcode.parsercore.ast.model.NodeType;
/**
@@ -10,7 +12,19 @@ import com.agenticcode.parsercore.ast.model.NodeType;
* items 27/28) for its full property set or source text. Regenerated on every
* re-ingest.
* @param annotations every annotation name on this node, comma-joined (not just the matched one)
* @param annotationList item 227: the same annotations as a list, each with its attribute values
* ({@code {name: WriteThrough, attributes: {column: KOS_X}}}); attributes are empty for a
* marker annotation and for a node ingested before item 227
*/
public record AnnotationMatch(String id, NodeType type, String name, String sourceFile, int startLine, int endLine,
String annotations) {
String annotations, List<AnnotationUse> annotationList) {
/**
* Item 227: one annotation on a node.
*
* @param name the annotation's simple name
* @param attributes attribute name to value (string literals unquoted, same-class constants resolved)
*/
public record AnnotationUse(String name, java.util.Map<String, String> attributes) {
}
}

View File

@@ -10,6 +10,8 @@ import org.jspecify.annotations.Nullable;
* the path starts at a prop or local, so only its tail is known; {@code component}/{@code attribute}
* say where the expression was passed ({@code SmartInput} / {@code field}). The {@code counterpart*}
* columns are the field's {@code COUNTERPART_OF} twin in the backend project (item 193), null when unlinked.
* Item 231: Angular form controls and ag-grid columns are bindings too; {@code sourceFile} is the file
* {@code lineNo} points into — the component's template for a template binding.
*/
public record Binding(String mode, String dto, String field, @Nullable String path, @Nullable String rootDto,
@Nullable String kind, boolean partial, @Nullable String component, @Nullable String attribute,

View File

@@ -0,0 +1,13 @@
package com.agenticcode.neo4jstore.graph;
/**
* Item 218: one edge of the call graph rolled up by directory.
*
* @param fromGroup the calling modules' directory prefix ({@code .} for the project root)
* @param toGroup the called modules' directory prefix
* @param calls module pairs from one group to the other
* @param callers distinct calling modules
* @param callees distinct called modules
*/
public record CallGraphGroup(String fromGroup, String toGroup, int calls, int callers, int callees) {
}

View File

@@ -19,8 +19,16 @@ import org.jspecify.annotations.Nullable;
* name resolves to more than one definition; {@code null} for an unresolved placeholder
* @param boundReads item 195: binding sites reading this field through a generated {@code Fields} path
* object (TypeScript DTOs only; 0 elsewhere), {@code boundWrites} those writing it
* @param adabasShortName item 236: a NatJav DDM column's ADABAS short name ({@code AA}), else {@code null}
* @param adabasOptions item 236: its FDT options ({@code DE NU}), else {@code null}
* @param fdt item 236: the FDT class the DDM points into ({@code FDT0080}), else {@code null}
* @param reads item 256: for a NatJav DDM column, the statements reading it through any view of
* the table across the project; {@code null} where not counted
* @param writes item 256: the same for writing statements
*/
public record DataStructureField(String name, String type, @Nullable String dataType, @Nullable String value,
String parent, int startLine, int endLine, @Nullable String scope,
@Nullable String sourceFile, int boundReads, int boundWrites) {
@Nullable String sourceFile, int boundReads, int boundWrites,
@Nullable String adabasShortName, @Nullable String adabasOptions,
@Nullable String fdt, @Nullable Integer reads, @Nullable Integer writes) {
}

View File

@@ -16,7 +16,9 @@ import java.util.List;
* the access occurs; {@code null} for direct accesses by the queried module
* @param sites per-statement provenance (line number + the file it lives in + copycode flag); empty
* for {@code DECLARES} rows (view/entity mapping, no statement)
* @param mappedBy item 211: the modules that define the table ({@code MAPS_TO}) — the DDM for
* Natural/NatJav, the {@code @Entity} for Java; empty when none is ingested
*/
public record DbAccess(String name, String mode, List<Integer> lineNos, @Nullable String via,
List<AccessSite> sites) {
List<AccessSite> sites, List<String> mappedBy) {
}

View File

@@ -0,0 +1,22 @@
package com.agenticcode.neo4jstore.graph;
import org.jspecify.annotations.Nullable;
import java.util.List;
/**
* Item 219: one table of a project.
*
* @param name the table (Natural: the ADABAS file name from the DDM; Java: the {@code @Table})
* @param mappedBy the modules defining it via {@code MAPS_TO} (DDM, {@code @Entity}); may be empty
* @param fdt item 236: the FDT class the mapping NatJav DDM points into ({@code FDT0080}), else {@code null}
* @param fileNumber item 236: that FDT's ADABAS file number ({@code 80}), else {@code null}
* @param readers distinct modules reading it — only with {@code stats=true}, else {@code null}
* @param writers distinct modules writing it — only with {@code stats=true}
* @param reads read statements — only with {@code stats=true}
* @param writes write statements — only with {@code stats=true}
*/
public record DbTableInfo(String name, List<String> mappedBy, @Nullable String fdt, @Nullable String fileNumber,
@Nullable Integer readers, @Nullable Integer writers,
@Nullable Integer reads, @Nullable Integer writes) {
}

View File

@@ -28,8 +28,14 @@ import java.util.List;
* @param guardValues every {@code VALUE} alternative of the branch, in source order and including
* blank ones — {@code VALUE 'GENAGREE-WOUT-SP', ' '} reports both, recording that
* a blank {@code guardField} also routes here (item 64)
* @param assignedField the field assigned in the branch (e.g. {@code #P-CALLED-PROG})
* @param assignedValue the assigned literal, quotes stripped (e.g. {@code WGEAGB0S})
* @param assignedField the field assigned in the branch (e.g. {@code #P-CALLED-PROG}); {@code null} for a
* call row (item 217)
* @param assignedValue the assigned literal, quotes stripped (e.g. {@code WGEAGB0S}); {@code null} for a
* call row
* @param target item 217 — for a call made in the branch, the called subroutine/module
* ({@code GET_DATA}); {@code null} for an assignment row
* @param edgeKind item 217 — the call's kind ({@code PERFORM}, {@code CALLNAT}, {@code FETCH}, …);
* {@code null} for an assignment row
* @param lineNo source line of the assignment, <em>within {@link #sourceFile}</em>
* @param sourceFile item 122 — the file {@code lineNo} is in. Usually the module's own file, but for a
* row spliced in from a copycode it is that {@code .cpy}. Until this was carried
@@ -45,7 +51,8 @@ import java.util.List;
* A nested include is otherwise invisible (item 104).
*/
public record DispatchEntry(List<DispatchGuard> guards, String guardField, String guardValue,
List<String> guardValues, String assignedField, String assignedValue,
List<String> guardValues, @Nullable String assignedField,
@Nullable String assignedValue, @Nullable String target, @Nullable String edgeKind,
int lineNo, String sourceFile, @Nullable String viaCopycode,
@Nullable Integer includedAt, List<IncludeStep> includePath) {
}

View File

@@ -0,0 +1,35 @@
package com.agenticcode.neo4jstore.graph;
import jakarta.enterprise.context.ApplicationScoped;
import org.jspecify.annotations.Nullable;
import java.util.concurrent.ConcurrentHashMap;
/**
* Item 242: the phase an ingest of a project is in right now, one line per project ("persisting files
* 201-400 of 2056", "finalize call_graph [12/45] link-counterparts-rest"). The ingest writes it at the
* points it already logs; the refresh job reads it so an asynchronous caller sees how far the run has
* got without reading the server log.
*
* <p>Keyed by project rather than by job, because the writers — the persist loop, the finalize step
* loop — know the project and nothing about jobs. That is unambiguous for the whole-project refreshes,
* which run one at a time per project; a lazy deep ingest of single modules writes here too, and then
* the line shows what the server is doing on that project, which is still the truth.
*/
@ApplicationScoped
public class IngestProgress {
private final ConcurrentHashMap<String, String> current = new ConcurrentHashMap<>();
public void report(String project, String phase) {
current.put(project, phase);
}
public @Nullable String current(String project) {
return current.get(project);
}
public void clear(String project) {
current.remove(project);
}
}

View File

@@ -15,6 +15,9 @@ import java.util.List;
* @param sloc source (non-blank, non-comment) lines, computed per language (item 46), or
* {@code null} if not yet stamped
* @param description one-line summary from the module's leading banner/Javadoc, or {@code null}
* @param owner item 208: the owning developer/department code from a NatJav migration header
* ({@code @OWNER}), or {@code null} (Natural, Java)
* @param traits item 222: generator model and online/batch indicators (NatJav only)
* @param callers aggregated callers with deduplicated {@code sourceFiles} index
* @param callees aggregated callees with deduplicated {@code sourceFiles} index
* @param sqlStatements full statement list; empty in lean (default) mode (P1-t)
@@ -30,6 +33,8 @@ public record ModuleContext(
@Nullable Integer loc,
@Nullable Integer sloc,
@Nullable String description,
@Nullable String owner,
ModuleTraits traits,
List<FunctionInfo> functions,
CallRefResponse callers,
CallRefResponse callees,

View File

@@ -24,9 +24,28 @@ import org.jspecify.annotations.Nullable;
* what makes same-simple-name classes distinguishable. UIs and the CLI show this
* instead. Equal to {@code name} for Natural, and {@code null} for a node ingested
* before the property existed
* @param generatorModel item 222: the Construct model of a generated NatJav program, or {@code null}
* @param usesMap item 222: {@code true} when the module displays a map, else {@code null}
* @param usesWorkFile item 222: {@code true} when it reads/writes a work file, else {@code null}
* @param checksDeviceBatch item 222: {@code true} when it tests {@code *DEVICE} for {@code 'BATCH'}, else
* {@code null}
* @param callerCount item 218: distinct modules calling this one; set only when the listing asked
* for it ({@code withCallerCount}, {@code noCallers}, {@code sort=fanIn})
*/
public record ModuleInfo(String name, String sourceFile, @Nullable String moduleKind,
@Nullable Integer loc, @Nullable Integer sloc,
@Nullable String ingestStatus, @Nullable String ingestDepth,
@Nullable String simpleName) {
@Nullable String simpleName, @Nullable String generatorModel,
@Nullable Boolean usesMap, @Nullable Boolean usesWorkFile,
@Nullable Boolean checksDeviceBatch, @Nullable Integer callerCount) {
/**
* The pre-item-222 shape: no traits, no caller count.
*/
public ModuleInfo(String name, String sourceFile, @Nullable String moduleKind, @Nullable Integer loc,
@Nullable Integer sloc, @Nullable String ingestStatus, @Nullable String ingestDepth,
@Nullable String simpleName) {
this(name, sourceFile, moduleKind, loc, sloc, ingestStatus, ingestDepth, simpleName,
null, null, null, null, null);
}
}

View File

@@ -0,0 +1,13 @@
package com.agenticcode.neo4jstore.graph;
/**
* Item 218: the fan-in options of the module listing.
*
* @param withCallerCount report {@code callerCount} on every row
* @param noCallers only modules nothing calls (entry points, or dead code)
* @param sortByFanIn most-called first
*/
public record ModuleListFanIn(boolean withCallerCount, boolean noCallers, boolean sortByFanIn) {
public static final ModuleListFanIn NONE = new ModuleListFanIn(false, false, false);
}

View File

@@ -0,0 +1,19 @@
package com.agenticcode.neo4jstore.graph;
import org.jspecify.annotations.Nullable;
/**
* Item 222: what a NatJav module does that tells an online dialog from a batch step, and the Construct
* model a generated program came from. Each flag is {@code true} or {@code null} (not recorded) — never
* an explicit {@code false}, because Natural and Java ingests do not record them at all.
*
* @param generatorModel {@code *SAG GENERATOR:} of the migration header ({@code GRAWE-DIALOG}, …)
* @param usesMap the module displays a map ({@code INPUT USING MAP})
* @param usesWorkFile the module reads or writes a work file
* @param checksDeviceBatch the module tests {@code *DEVICE} against {@code 'BATCH'}
*/
public record ModuleTraits(@Nullable String generatorModel, @Nullable Boolean usesMap,
@Nullable Boolean usesWorkFile, @Nullable Boolean checksDeviceBatch) {
public static final ModuleTraits NONE = new ModuleTraits(null, null, null, null);
}

View File

@@ -0,0 +1,30 @@
package com.agenticcode.neo4jstore.graph;
import org.jspecify.annotations.Nullable;
import java.util.List;
/**
* Item 230: a screen of an Angular front end — a route or a dialog.
*
* @param kind {@code route} or {@code dialog}
* @param name the route's full path ({@code konto/:mandant/:oart/:kontoId}), or the dialog's class
* @param component the module (file) of the component shown, {@code null} for a redirect-only route
* @param componentClass the component class
* @param title the route title
* @param guards the route's guards, comma-joined
* @param redirectTo where a redirect route leads
* @param sourceFile the routing file, or the dialog component's file
* @param line the route's line
* @param openedBy for a dialog: every call site that opens it
*/
public record Screen(String kind, String name, @Nullable String component, @Nullable String componentClass,
@Nullable String title, @Nullable String guards, @Nullable String redirectTo,
@Nullable String sourceFile, @Nullable Integer line, List<Opener> openedBy) {
/**
* One place that opens a dialog.
*/
public record Opener(String module, @Nullable String callerFn, @Nullable Integer line) {
}
}

View File

@@ -0,0 +1,34 @@
package com.agenticcode.neo4jstore.graph;
import org.jspecify.annotations.Nullable;
import java.util.List;
/**
* Item 225: one operation of a SOAP service.
*
* @param kind {@code axis2} (a {@code services.xml} service), {@code wsdl} (a contract file)
* or {@code jaxws} (a {@code @WebService} class)
* @param service the service name
* @param operation the operation, or {@code null} for a service without any
* @param implementation the class that runs it ({@code ServiceClass} / the {@code @WebService} class);
* {@code null} for a WSDL contract
* @param sourceFile the file declaring it
* @param line its line
* @param location the WSDL's {@code soap:address}, else {@code null}
* @param counterpartProject the project of the {@code COUNTERPART_OF {via: soap}} twin, or {@code null}
* @param counterpartService that twin's name
* @param port item 238: the EntireX port the service exposes ({@code WSK369}) — from an Axis2
* action mapping or the WSDL portType; {@code null} for JAX-WS rows
* @param clientPackage item 239: for a WSDL row, the Java package its wsimport client is generated into
* ({@code at.k3.entirex.ws.wsk386}), from the project's wsimport binding file
* @param clients item 239: the modules importing or calling a class of that package
* @param typesPackages item 258: for a WSDL row, the comma-separated Java packages JAXB generates the
* request/response classes of its schemas into ({@code rpc.entirex.softwareag.com.wsk369})
*/
public record SoapEndpoint(String kind, String service, @Nullable String operation, @Nullable String implementation,
String sourceFile, @Nullable Integer line, @Nullable String location,
@Nullable String counterpartProject, @Nullable String counterpartService,
@Nullable String port, @Nullable String clientPackage, List<String> clients,
@Nullable String typesPackages) {
}

View File

@@ -0,0 +1,20 @@
package com.agenticcode.neo4jstore.graph;
import org.jspecify.annotations.Nullable;
/**
* Items 216/221: one occurrence of a statement recorded as a {@code STATEMENT} node — a transaction
* boundary, {@code REINPUT}, {@code SET KEY}, {@code STACK DATA} or an {@code ON ERROR} block.
*
* @param kind {@code END_TRANSACTION}, {@code BACKOUT_TRANSACTION}, {@code REINPUT}, {@code SET_KEY},
* {@code STACK_DATA} or {@code ON_ERROR}
* @param module the module the statement runs in
* @param function the subroutine it runs in, or {@code null} for the module body
* @param sourceFile the file {@code lineNo} is in
* @param lineNo the statement's line
* @param viaCopycode the copycode the statement was expanded from, or {@code null}
* @param includedAt the line of that expansion in the module, or {@code null}
*/
public record StatementSite(String kind, String module, @Nullable String function, String sourceFile, int lineNo,
@Nullable String viaCopycode, @Nullable Integer includedAt) {
}

View File

@@ -10,6 +10,9 @@ package com.agenticcode.neo4jstore.graph;
* @param originFile the file the call site lives in (the module's own file, or an included copycode)
* @param lineNo the line of the {@code CALLNAT} within {@code originFile}
* @param variable the dispatch variable name (e.g. {@code #GETSHORT-MODUL})
* @param statementKind item 220: {@code CALLNAT}, or for NatJav {@code FETCH}, {@code FETCH_RETURN},
* {@code INPUT_MAP} — the statement behind the dynamic call
*/
public record UnresolvedDynamicCall(String module, String originFile, int lineNo, String variable) {
public record UnresolvedDynamicCall(String module, String originFile, int lineNo, String variable,
String statementKind) {
}

View File

@@ -0,0 +1,15 @@
package com.agenticcode.neo4jstore.graph;
import java.util.List;
/**
* Item 220: the unresolved dynamic call sites sharing one dispatch variable — where one override
* or one constant-propagation fix would settle many sites at once.
*
* @param variable the dispatch variable
* @param sites how many unresolved sites use it
* @param modules the distinct calling modules, sorted
* @param statementKinds the distinct statement kinds among the sites, sorted
*/
public record UnresolvedDynamicCallGroup(String variable, int sites, List<String> modules, List<String> statementKinds) {
}

View File

@@ -38,5 +38,42 @@ public enum CallKind {
* than parsed literally — edges carrying this kind are inferred from the macro contract, not
* statically certain.
*/
INCLUDE_MACRO
INCLUDE_MACRO,
/**
* Natural {@code FETCH '<program>'} (item 206) — transfers control to a program without
* returning. Recorded for a literal target only; a {@code FETCH <variable>} stays
* {@link #CALLNAT_DYNAMIC} (with edge property {@code statement = FETCH}) so the dynamic-call
* resolution still applies to it.
*/
FETCH,
/**
* Natural {@code FETCH RETURN '<program>'} (item 206) — runs a program and returns to the caller.
* Literal target only, like {@link #FETCH}.
*/
FETCH_RETURN,
/**
* Natural {@code INPUT USING MAP '<map>'} (item 206) — the module displays the map. The target is
* the {@code MAP} module, so a map finally has callers.
*/
INPUT_MAP,
/**
* Natural {@code STACK TOP COMMAND '<program>'} (item 206) — puts a program on the stack; it
* runs when the current one ends. Recorded only for a single-word literal: a command with
* parameters ({@code 'LOGON KORIN'}) is a system command, not a module.
*/
STACK_COMMAND,
/**
* Item 229: an Angular template uses another component or directive — an element or attribute that
* matches its selector ({@code <app-user-display>}). The call site is in the template file
* ({@code originFile}).
*/
TEMPLATE,
/**
* Item 230: {@code x.open(SomeComponent, …)} — MatDialog or a wrapper — opens a dialog component.
*/
DIALOG,
/**
* Item 230: a route ({@code FUNCTION kind=route}, named {@code <path>@<line>}) shows its component.
*/
ROUTE
}

View File

@@ -57,5 +57,20 @@ public enum NodeType {
/**
* Item 196: one style block — an {@code sx}/{@code style}/{@code styled()} literal or a CSS rule.
*/
STYLE
STYLE,
/**
* Items 216/221: one occurrence of a Natural statement that is neither a call nor a data access
* but still matters to an agent — {@code END TRANSACTION}, {@code BACKOUT TRANSACTION},
* {@code REINPUT}, {@code SET KEY}, {@code STACK DATA}, an {@code ON ERROR} block. The kind is the
* {@code statement} property; the {@code name} is the synthetic key {@code <KIND>@<line>} (like
* {@link #COMMENT}'s) so the identifier search, which matches every name, never mistakes it for
* a declaration. {@code CONTAINS}ed by the module or subroutine it runs in.
*/
STATEMENT,
/**
* Item 226: one setting of a configuration file — a {@code .properties} key or a flattened XML path
* ({@code persistence/persistence-unit[KOS]/jta-data-source}) — with the value (secrets masked) as
* {@code value}. {@code CONTAINS}ed by the file's {@code MODULE} ({@code moduleKind: CONFIG}).
*/
CONFIG_ENTRY
}

View File

@@ -30,6 +30,11 @@
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
</dependency>
<!-- Item 253: OpenAPI specs (swagger.yaml) of spec-first Spring servers -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>

View File

@@ -0,0 +1,245 @@
package com.agenticcode.parserjava;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import org.jspecify.annotations.Nullable;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.XMLConstants;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Pattern;
/**
* Item 226: configuration files as graph data — {@code .properties} and the XML descriptors that wire an
* application (JPA {@code persistence.xml}, JAX-WS {@code sun-jaxws.xml}, {@code web.xml}, Spring
* contexts). The file becomes a {@code MODULE} ({@code moduleKind: CONFIG}, named by its path, so two
* {@code application.properties} never collide) and each setting a {@link NodeType#CONFIG_ENTRY}
* {@code CONTAINS}ed by it: {@code name} = the key, {@code value} = the value, at its line — so
* {@code search/value?value=KOSDB} finds the datasource that names it.
*
* <p>XML is flattened to keys by element path; an element's {@code name}/{@code id} attribute becomes a
* discriminator, so {@code <property name="hibernate.dialect" value="…"/>} under a persistence unit is
* {@code persistence/persistence-unit[KOS]/properties/property[hibernate.dialect]@value}.
*
* <p><b>Secrets are masked before anything is stored.</b> A key that names a password, secret, token,
* credential or API key gets the value {@code ***}, and a {@code password=…} inside a value (a JDBC URL)
* is masked in place: KORIN's NATPARM files hold database passwords in clear text, and the graph is
* readable by every API client. Parsing is XXE-safe (no DTDs, no external entities).
*/
public final class ConfigFileParser {
public static final String LANGUAGE = "config";
public static final String CONFIG_KIND = "CONFIG";
static final String MASK = "***";
private static final Pattern SECRET_KEY = Pattern.compile(
"(?i)(password|passwd|pwd|secret|token|credential|api[_.-]?key|private[_.-]?key)");
private static final Pattern SECRET_IN_VALUE = Pattern.compile(
"(?i)((?:password|passwd|pwd)\\s*=\\s*)[^;&\\s]+");
private ConfigFileParser() {
}
/**
* @return the file's module and its entries, or just the module when the content cannot be read
*/
public static ParseResult parse(String sourceFile, String content) {
List<AstNode> nodes = new ArrayList<>();
List<AstEdge> edges = new ArrayList<>();
AstNode file = new AstNode(UUID.randomUUID(), NodeType.MODULE, sourceFile, sourceFile, LANGUAGE, 1,
Math.max(1, (int) content.lines().count()), null, null, Map.of("moduleKind", CONFIG_KIND));
nodes.add(file);
List<Entry> entries = sourceFile.toLowerCase(Locale.ROOT).endsWith(".xml")
? xmlEntries(content) : propertiesEntries(content);
for (Entry entry : entries) {
AstNode node = new AstNode(UUID.randomUUID(), NodeType.CONFIG_ENTRY, entry.key(), sourceFile, LANGUAGE,
entry.line(), entry.line(), null, mask(entry.key(), entry.value()), null);
nodes.add(node);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CONTAINS, file.id(), node.id(), entry.line(), null));
}
return new ParseResult(nodes, edges);
}
/**
* @return {@code value} with secrets masked — all of it when the key names one
*/
static String mask(String key, String value) {
if (SECRET_KEY.matcher(key).find()) {
return MASK;
}
return SECRET_IN_VALUE.matcher(value).replaceAll("$1" + MASK);
}
record Entry(String key, String value, int line) {
}
/**
* {@code key=value}, {@code key: value} or {@code key value}; {@code #}/{@code !} comments; a trailing
* backslash continues the value on the next line (the entry keeps the first line's number).
*/
static List<Entry> propertiesEntries(String content) {
List<Entry> entries = new ArrayList<>();
String[] lines = content.split("\\R", -1);
for (int i = 0; i < lines.length; i++) {
String line = lines[i].strip();
if (line.isEmpty() || line.startsWith("#") || line.startsWith("!")) {
continue;
}
int startLine = i + 1;
StringBuilder logical = new StringBuilder();
String current = line;
while (current.endsWith("\\") && i + 1 < lines.length) {
logical.append(current, 0, current.length() - 1);
current = lines[++i].strip();
}
logical.append(current);
String text = logical.toString();
int sep = separator(text);
String key = sep < 0 ? text : text.substring(0, sep).strip();
String value = sep < 0 ? "" : text.substring(sep).strip();
// `key = value`: whitespace ends the key, and an `=`/`:` after it still belongs to the separator.
if (value.startsWith("=") || value.startsWith(":")) {
value = value.substring(1).strip();
}
if (!key.isEmpty()) {
entries.add(new Entry(key, value, startLine));
}
}
return entries;
}
private static int separator(String text) {
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '\\') {
i++;
} else if (c == '=' || c == ':' || Character.isWhitespace(c)) {
return i;
}
}
return -1;
}
static List<Entry> xmlEntries(String content) {
List<Entry> entries = new ArrayList<>();
try {
newSaxParser().parse(new InputSource(new StringReader(content)), new Flattener(entries));
} catch (SAXException | IOException e) {
// A broken descriptor keeps its module and whatever was read before the error.
}
return entries;
}
static SAXParser newSaxParser() {
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
return factory.newSAXParser();
} catch (Exception e) {
throw new IllegalStateException("cannot create a secure SAX parser", e);
}
}
/**
* Turns elements into path keys: attributes become {@code path@attr} entries, leaf text a
* {@code path} entry.
*/
private static final class Flattener extends DefaultHandler {
private final List<Entry> entries;
private final Deque<Frame> path = new ArrayDeque<>();
private @Nullable Locator locator;
Flattener(List<Entry> entries) {
this.entries = entries;
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
if (!path.isEmpty()) {
path.peek().hasChildren = true;
}
String element = localName.isEmpty() ? qName : localName;
String discriminator = attributes.getValue("name") != null ? attributes.getValue("name")
: attributes.getValue("id");
String segment = discriminator != null ? element + "[" + discriminator + "]" : element;
String prefix = path.isEmpty() ? segment : path.peek().key + "/" + segment;
int line = locator == null ? 1 : locator.getLineNumber();
path.push(new Frame(prefix, line));
for (int i = 0; i < attributes.getLength(); i++) {
String attribute = attributes.getLocalName(i).isEmpty() ? attributes.getQName(i) : attributes.getLocalName(i);
if (attribute.startsWith("xmlns") || attribute.equals("name") || attribute.equals("id")
|| attributes.getQName(i).startsWith("xmlns")) {
continue;
}
entries.add(new Entry(prefix + "@" + attribute, attributes.getValue(i), line));
}
}
@Override
public void characters(char[] ch, int start, int length) {
if (!path.isEmpty()) {
path.peek().text.append(ch, start, length);
}
}
@Override
public void endElement(String uri, String localName, String qName) {
Frame frame = path.pop();
String text = frame.text.toString().strip();
if (!frame.hasChildren && !text.isEmpty()) {
entries.add(new Entry(frame.key, text, frame.line));
}
}
}
private static final class Frame {
private final String key;
private final int line;
private final StringBuilder text = new StringBuilder();
private boolean hasChildren;
Frame(String key, int line) {
this.key = key;
this.line = line;
}
}
/**
* @return the XML-attribute map of a SAX start tag — convenience for the WSDL and Axis2 readers
*/
static Map<String, String> attributes(Attributes attributes) {
Map<String, String> map = new HashMap<>();
for (int i = 0; i < attributes.getLength(); i++) {
String name = attributes.getLocalName(i).isEmpty() ? attributes.getQName(i) : attributes.getLocalName(i);
map.put(name, attributes.getValue(i));
}
return map;
}
}

View File

@@ -0,0 +1,27 @@
package com.agenticcode.parserjava;
import com.agenticcode.parsercore.ast.model.LocMetrics;
import com.agenticcode.parsercore.ast.spi.LineCounter;
/**
* Items 225/226: LoC/SLoC of configuration and descriptor files — a source line is any non-blank line
* that is not a {@code #}/{@code !} properties comment. XML comments are not stripped: they are rare
* in descriptors and a line-based count is what the metric promises.
*/
public final class ConfigLineCounter implements LineCounter {
@Override
public String language() {
return ConfigFileParser.LANGUAGE;
}
@Override
public LocMetrics count(String content) {
int loc = LocMetrics.physicalLineCount(content);
int sloc = (int) content.lines()
.map(String::strip)
.filter(line -> !line.isEmpty() && !line.startsWith("#") && !line.startsWith("!"))
.count();
return new LocMetrics(loc, sloc);
}
}

View File

@@ -0,0 +1,127 @@
package com.agenticcode.parserjava;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.CommentBlocks;
import com.agenticcode.parsercore.ast.model.CommentProperties;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.comments.BlockComment;
import com.github.javaparser.ast.comments.Comment;
import com.github.javaparser.ast.comments.JavadocComment;
import com.github.javaparser.ast.comments.LineComment;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
/**
* Comment blocks of a parsed Java compilation unit (roadmap item 141), shared by the Java and the
* NatJav parsers — the grouping and the DOCUMENTS attachment are identical for both, because NatJav
* <em>is</em> Java syntax.
*
* <p>What differs is the text: a transpiled file also carries the transpiler's own bookkeeping, which
* is why {@code retainedText} lets a caller drop parts of a block before it becomes a node. The Java
* parser passes the identity — a hand-written file has nothing to strip.
*/
public final class JavaComments {
private JavaComments() {
}
/**
* One {@link NodeType#COMMENT} node per contiguous comment block, each with a
* {@link EdgeType#DOCUMENTS} edge to the declaration it documents.
*
* <p>Comments are free here: JavaParser already attaches them to the {@link CompilationUnit}
* (the default {@code ParserConfiguration.setAttributeComments(true)}), so this collects what the
* parse produced rather than re-lexing. Adjacent {@code //} lines are folded into one block —
* a five-line {@code //} preamble is one comment, not five.
*/
public static void collect(CompilationUnit unit, String language, String sourceFile,
List<AstNode> nodes, List<AstEdge> edges, UnaryOperator<String> retainedText) {
List<Comment> comments = unit.getAllComments().stream()
.filter(c -> c.getBegin().isPresent() && c.getEnd().isPresent())
.sorted(Comparator.comparingInt(c -> c.getBegin().orElseThrow().line))
.toList();
if (comments.isEmpty()) {
return;
}
// Declaration nodes of THIS file only: placeholder modules for referenced classes carry
// sourceFile "" and are not a target a comment in this file could ever document.
List<AstNode> targets = nodes.stream()
.filter(n -> n.sourceFile().equals(sourceFile))
.filter(CommentBlocks::isDocumentable)
.toList();
if (targets.isEmpty()) {
return; // nothing parsed from this file — a comment with no target is not emitted
}
List<Comment> block = new ArrayList<>();
for (Comment comment : comments) {
boolean extendsBlock = !block.isEmpty()
&& comment instanceof LineComment
&& block.get(block.size() - 1) instanceof LineComment previous
&& previous.getEnd().orElseThrow().line + 1 == comment.getBegin().orElseThrow().line;
if (!extendsBlock && !block.isEmpty()) {
emitCommentBlock(block, language, sourceFile, targets, nodes, edges, retainedText);
block = new ArrayList<>();
}
block.add(comment);
}
emitCommentBlock(block, language, sourceFile, targets, nodes, edges, retainedText);
}
private static void emitCommentBlock(List<Comment> block, String language, String sourceFile,
List<AstNode> targets, List<AstNode> nodes, List<AstEdge> edges,
UnaryOperator<String> retainedText) {
if (block.isEmpty()) {
return;
}
int startLine = block.get(0).getBegin().orElseThrow().line;
int endLine = block.get(block.size() - 1).getEnd().orElseThrow().line;
String text = retainedText.apply(
block.stream().map(JavaComments::commentText).collect(Collectors.joining("\n")));
@Nullable AstNode commentNode = CommentBlocks.commentNode(language, sourceFile, startLine, endLine,
text, commentKind(block.get(0)));
if (commentNode == null) {
return; // an empty /**/ or a bare // documents nothing
}
nodes.add(commentNode);
edges.add(CommentBlocks.documentsEdge(commentNode,
CommentBlocks.documentedNode(targets, startLine, endLine)));
}
/**
* A comment's text with its decoration removed. {@code getContent()} hands back the inner text
* verbatim, which for a block/Javadoc comment still carries every line's indentation and leading
* {@code *} — so the stored text read {@code "* ServiceEndpoint: …\n\t * UpmsObject: …"}. Natural
* comments already lose their markers, and a reader (human or agent) wants the same thing here:
* the sentence, not the comment syntax.
*/
private static String commentText(Comment comment) {
if (comment instanceof LineComment) {
return comment.getContent().strip();
}
return comment.getContent().lines()
.map(line -> {
String stripped = line.strip();
return stripped.startsWith("*") ? stripped.substring(1).stripLeading() : stripped;
})
.collect(Collectors.joining("\n"))
.strip();
}
private static String commentKind(Comment comment) {
if (comment instanceof JavadocComment) {
return CommentProperties.KIND_JAVADOC;
}
return comment instanceof BlockComment ? CommentProperties.KIND_BLOCK : CommentProperties.KIND_LINE;
}
}

View File

@@ -21,6 +21,7 @@ import org.jspecify.annotations.Nullable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
/**
@@ -433,6 +434,7 @@ public final class JavaParser implements LanguageParser {
@Nullable String typeAnnotations = annotationNames(type);
if (typeAnnotations != null) {
moduleProps.put("annotations", typeAnnotations);
putAnnotationAttributes(moduleProps, type, constants);
}
// Item 130: the class-level @Path, so "which endpoint path reaches this handler" is
// answerable from the graph. Annotations are otherwise stored by name only, and a JAX-RS
@@ -441,6 +443,12 @@ public final class JavaParser implements LanguageParser {
if (typePath != null) {
moduleProps.put("restPath", typePath);
}
// Item 254: a class path that cannot be resolved would put every handler of the class on a wrong
// path; the class's methods are then no endpoints (see below), the expression stays visible.
@Nullable String typePathUnresolved = unresolvedRestPath(type, typePath);
if (typePathUnresolved != null) {
moduleProps.put(REST_PATH_UNRESOLVED, typePathUnresolved);
}
AstNode typeNode = node(NodeType.MODULE, fqn, sourceFile,
type.getBegin().map(p -> p.line).orElse(1),
type.getEnd().map(p -> p.line).orElse(1),
@@ -502,6 +510,7 @@ public final class JavaParser implements LanguageParser {
Map<String, String> props = columnProperties(field, column, variable, className, constants);
if (fieldAnnotations != null) {
props.put("annotations", fieldAnnotations);
putAnnotationAttributes(props, field, constants);
}
fieldNode = node(NodeType.FIELD, variable.getNameAsString(), sourceFile,
startLine, endLine, dataType, null, props);
@@ -510,6 +519,7 @@ public final class JavaParser implements LanguageParser {
Map<String, String> fieldProps = new LinkedHashMap<>();
if (fieldAnnotations != null) {
fieldProps.put("annotations", fieldAnnotations);
putAnnotationAttributes(fieldProps, field, constants);
}
if (value != null && templateConstants.contains(variable.getNameAsString())) {
fieldProps.put("valueKind", "template"); // item 139: `"...".formatted(...)`
@@ -577,6 +587,7 @@ public final class JavaParser implements LanguageParser {
Map<String, String> methodProps = new HashMap<>();
if (methodAnnotations != null) {
methodProps.put("annotations", methodAnnotations);
putAnnotationAttributes(methodProps, method, constants);
}
// Item 130: the method half of a JAX-RS endpoint — its own @Path (often absent, which
// means "the class path itself") and the HTTP verb annotation.
@@ -584,8 +595,15 @@ public final class JavaParser implements LanguageParser {
if (methodPath != null) {
methodProps.put("restPath", methodPath);
}
// Item 254: a mapping whose path is there but not resolvable is not reported on the class
// path — a wrong path is worse than a missing one. No verb, so no endpoint; the expression
// is kept for `nodes/{id}`.
@Nullable String methodPathUnresolved = unresolvedRestPath(method, methodPath);
if (methodPathUnresolved != null) {
methodProps.put(REST_PATH_UNRESOLVED, methodPathUnresolved);
}
@Nullable String httpMethod = httpMethod(method);
if (httpMethod != null) {
if (httpMethod != null && methodPathUnresolved == null && typePathUnresolved == null) {
methodProps.put("httpMethod", httpMethod);
}
// Item 33: modifier-derived kind, so an agent can ask "what must a subclass
@@ -610,7 +628,7 @@ public final class JavaParser implements LanguageParser {
ctor.getBegin().map(p -> p.line).orElse(1),
ctor.getEnd().map(p -> p.line).orElse(1),
className, null,
ctorAnnotations != null ? Map.of("annotations", ctorAnnotations) : null);
ctorAnnotations != null ? ctorAnnotationProps(ctorAnnotations, ctor, constants) : null);
nodes.add(ctorNode);
edges.add(edge(EdgeType.CONTAINS, typeNode.id(), ctorNode.id(), ctorNode.startLine()));
callables.add(ctor);
@@ -712,9 +730,10 @@ public final class JavaParser implements LanguageParser {
addReferenceEdges(unit, type, typeNode, referencedModules, types, nodes, edges);
}
// Item 141: comments are file-level, so this runs once after the type loop — inside it, a file
// with nested types would emit every comment once per type.
addCommentNodes(unit, sourceFile, nodes, edges);
JavaComments.collect(unit, "java", sourceFile, nodes, edges, UnaryOperator.identity());
return new ParseResult(nodes, edges);
}
@@ -833,6 +852,55 @@ public final class JavaParser implements LanguageParser {
return field.getAnnotationByName(name);
}
/**
* Item 227: the attribute values of every annotation on {@code declaration}, flattened to
* {@code Name.attr=value} entries joined by US ({@code \u001F}) — {@code @WriteThrough(column = "X")}
* becomes {@code WriteThrough.column=X}, a single-member {@code @Path("/a")} {@code Path.value=/a}.
* A string literal is unquoted, a same-class constant resolved, anything else kept as written
* ({@code Type.READ_WRITE}); an array lists its elements comma-separated. Stored as the node
* property {@code annotationAttributes}; absent when no annotation has an attribute.
*/
private static void putAnnotationAttributes(Map<String, String> props, NodeWithAnnotations<?> declaration,
Map<String, String> constants) {
List<String> entries = new ArrayList<>();
for (AnnotationExpr annotation : declaration.getAnnotations()) {
String name = annotation.getNameAsString();
if (annotation instanceof com.github.javaparser.ast.expr.NormalAnnotationExpr normal) {
for (com.github.javaparser.ast.expr.MemberValuePair pair : normal.getPairs()) {
entries.add(name + "." + pair.getNameAsString() + "=" + annotationValue(pair.getValue(), constants));
}
} else if (annotation instanceof com.github.javaparser.ast.expr.SingleMemberAnnotationExpr single) {
entries.add(name + ".value=" + annotationValue(single.getMemberValue(), constants));
}
}
if (!entries.isEmpty()) {
props.put(ANNOTATION_ATTRIBUTES, String.join("\u001F", entries));
}
}
private static Map<String, String> ctorAnnotationProps(String names, ConstructorDeclaration ctor,
Map<String, String> constants) {
Map<String, String> props = new HashMap<>();
props.put("annotations", names);
putAnnotationAttributes(props, ctor, constants);
return props;
}
private static String annotationValue(Expression value, Map<String, String> constants) {
if (value instanceof StringLiteralExpr literal) {
return literal.asString();
}
if (value instanceof com.github.javaparser.ast.expr.ArrayInitializerExpr array) {
return array.getValues().stream().map(v -> annotationValue(v, constants)).collect(Collectors.joining(","));
}
if (value instanceof NameExpr name && constants.containsKey(name.getNameAsString())) {
return constants.get(name.getNameAsString());
}
return value.toString();
}
static final String ANNOTATION_ATTRIBUTES = "annotationAttributes";
/**
* Comma-joined, source-order-distinct simple names of every annotation on {@code declaration}
* (item 29: generic annotation search, independent of any annotation's specific interpretation
@@ -865,16 +933,58 @@ public final class JavaParser implements LanguageParser {
@Nullable
private static String restPath(NodeWithAnnotations<?> declaration, String className,
Map<String, String> constants) {
@Nullable Expression member = restPathMember(declaration);
return member == null ? null : resolveAnnotationString(member, className, constants);
}
static final String REST_PATH_UNRESOLVED = "restPathUnresolved";
/**
* Item 254: the source text of a declared path that {@link #restPath} could not resolve, or
* {@code null} when there is none or it resolved.
*/
@Nullable
private static String unresolvedRestPath(NodeWithAnnotations<?> declaration, @Nullable String resolved) {
if (resolved != null) {
return null;
}
@Nullable Expression member = restPathMember(declaration);
return member == null ? null : member.toString();
}
/**
* The expression a declaration's path annotation names: JAX-RS {@code @Path}, or — item 235 — Spring
* MVC {@code @RequestMapping} on class or method and {@code @Get/Post/…Mapping} on a method, path under
* {@code value} or its alias {@code path}; an array maps several paths, the first is taken.
*/
@Nullable
private static Expression restPathMember(NodeWithAnnotations<?> declaration) {
for (AnnotationExpr annotation : declaration.getAnnotations()) {
if (!annotation.getNameAsString().equals("Path")) {
continue;
String name = annotation.getNameAsString();
if (name.equals("Path")) {
return annotationMember(annotation, "value");
}
if (name.equals("RequestMapping") || SPRING_VERB_MAPPINGS.containsKey(name)) {
@Nullable Expression member = annotationMember(annotation, "value");
if (member == null) {
member = annotationMember(annotation, "path");
}
if (member instanceof com.github.javaparser.ast.expr.ArrayInitializerExpr array) {
member = array.getValues().isEmpty() ? null : array.getValues().get(0);
}
return member;
}
@Nullable Expression member = annotationMember(annotation, "value");
return member == null ? null : resolveAnnotationString(member, className, constants);
}
return null;
}
/**
* Item 235: Spring's verb-specific mapping annotations.
*/
private static final Map<String, String> SPRING_VERB_MAPPINGS = Map.of(
"GetMapping", "GET", "PostMapping", "POST", "PutMapping", "PUT",
"DeleteMapping", "DELETE", "PatchMapping", "PATCH");
/**
* Item 130: the HTTP verb a handler method is annotated with ({@code @GET}, {@code @POST}, …), or
* {@code null} when it carries none — which is how a sub-resource locator is told apart from an
@@ -883,8 +993,26 @@ public final class JavaParser implements LanguageParser {
@Nullable
private static String httpMethod(MethodDeclaration method) {
for (AnnotationExpr annotation : method.getAnnotations()) {
if (HTTP_METHOD_ANNOTATIONS.contains(annotation.getNameAsString())) {
return annotation.getNameAsString();
String name = annotation.getNameAsString();
if (HTTP_METHOD_ANNOTATIONS.contains(name)) {
return name;
}
if (SPRING_VERB_MAPPINGS.containsKey(name)) {
return SPRING_VERB_MAPPINGS.get(name);
}
// Item 235: a method-level @RequestMapping names its verb in `method = RequestMethod.X`;
// without one it serves every verb, recorded as ANY (the REST counterpart match treats it
// as a wildcard).
if (name.equals("RequestMapping")) {
@Nullable Expression verb = annotationMember(annotation, "method");
if (verb instanceof com.github.javaparser.ast.expr.ArrayInitializerExpr array) {
verb = array.getValues().isEmpty() ? null : array.getValues().get(0);
}
if (verb == null) {
return "ANY";
}
String text = verb.toString();
return text.substring(text.lastIndexOf('.') + 1);
}
}
return null;
@@ -912,14 +1040,24 @@ public final class JavaParser implements LanguageParser {
}
/**
* Resolves an annotation member value to a string: a string literal, or a same-class
* constant reference (bare {@code NAME} or {@code ClassName.NAME}); {@code null} otherwise.
* Resolves an annotation member value to a string: a string literal, a same-class constant
* reference (bare {@code NAME} or {@code ClassName.NAME}), or — item 254 — a {@code +} of those
* ({@code @PostMapping(path = KONTONUMMER_PATH + BUCHHALTUNG_POST_PATH)}); {@code null} as soon as
* one part is anything else.
*/
@Nullable
private static String resolveAnnotationString(Expression expr, String className, Map<String, String> constants) {
if (expr instanceof StringLiteralExpr s) {
return s.asString();
}
if (expr instanceof EnclosedExpr enclosed) {
return resolveAnnotationString(enclosed.getInner(), className, constants);
}
if (expr instanceof BinaryExpr binary && binary.getOperator() == BinaryExpr.Operator.PLUS) {
@Nullable String left = resolveAnnotationString(binary.getLeft(), className, constants);
@Nullable String right = left == null ? null : resolveAnnotationString(binary.getRight(), className, constants);
return left == null || right == null ? null : left + right;
}
@Nullable String referenced = referencedConstantName(expr, className);
if (referenced != null) {
return constants.get(referenced);
@@ -1608,95 +1746,6 @@ public final class JavaParser implements LanguageParser {
Map<String, String> resolved, Set<String> templates) {
}
/**
* Item 141: one {@link NodeType#COMMENT} node per contiguous comment block, each with a
* {@link EdgeType#DOCUMENTS} edge to the declaration it documents.
*
* <p>Comments are free here: JavaParser already attaches them to the {@link CompilationUnit}
* (the default {@code ParserConfiguration.setAttributeComments(true)}), so this collects what the
* parse produced rather than re-lexing. Adjacent {@code //} lines are folded into one block —
* a five-line {@code //} preamble is one comment, not five.
*/
private static void addCommentNodes(CompilationUnit unit, String sourceFile,
List<AstNode> nodes, List<AstEdge> edges) {
List<Comment> comments = unit.getAllComments().stream()
.filter(c -> c.getBegin().isPresent() && c.getEnd().isPresent())
.sorted(Comparator.comparingInt(c -> c.getBegin().orElseThrow().line))
.toList();
if (comments.isEmpty()) {
return;
}
// Declaration nodes of THIS file only: placeholder modules for referenced classes carry
// sourceFile "" and are not a target a comment in this file could ever document.
List<AstNode> targets = nodes.stream()
.filter(n -> n.sourceFile().equals(sourceFile))
.filter(CommentBlocks::isDocumentable)
.toList();
if (targets.isEmpty()) {
return; // nothing parsed from this file — a comment with no target is not emitted
}
List<Comment> block = new ArrayList<>();
for (Comment comment : comments) {
boolean extendsBlock = !block.isEmpty()
&& comment instanceof LineComment
&& block.get(block.size() - 1) instanceof LineComment previous
&& previous.getEnd().orElseThrow().line + 1 == comment.getBegin().orElseThrow().line;
if (!extendsBlock && !block.isEmpty()) {
emitCommentBlock(block, sourceFile, targets, nodes, edges);
block = new ArrayList<>();
}
block.add(comment);
}
emitCommentBlock(block, sourceFile, targets, nodes, edges);
}
private static void emitCommentBlock(List<Comment> block, String sourceFile, List<AstNode> targets,
List<AstNode> nodes, List<AstEdge> edges) {
if (block.isEmpty()) {
return;
}
int startLine = block.get(0).getBegin().orElseThrow().line;
int endLine = block.get(block.size() - 1).getEnd().orElseThrow().line;
String text = block.stream().map(JavaParser::commentText).collect(Collectors.joining("\n"));
@Nullable AstNode commentNode = CommentBlocks.commentNode("java", sourceFile, startLine, endLine,
text, commentKind(block.get(0)));
if (commentNode == null) {
return; // an empty /**/ or a bare // documents nothing
}
nodes.add(commentNode);
edges.add(CommentBlocks.documentsEdge(commentNode,
CommentBlocks.documentedNode(targets, startLine, endLine)));
}
/**
* A comment's text with its decoration removed. {@code getContent()} hands back the inner text
* verbatim, which for a block/Javadoc comment still carries every line's indentation and leading
* {@code *} — so the stored text read {@code "* ServiceEndpoint: …\n\t * UpmsObject: …"}. Natural
* comments already lose their markers, and a reader (human or agent) wants the same thing here:
* the sentence, not the comment syntax.
*/
private static String commentText(Comment comment) {
if (comment instanceof LineComment) {
return comment.getContent().strip();
}
return comment.getContent().lines()
.map(line -> {
String stripped = line.strip();
return stripped.startsWith("*") ? stripped.substring(1).stripLeading() : stripped;
})
.collect(Collectors.joining("\n"))
.strip();
}
private static String commentKind(Comment comment) {
if (comment instanceof JavadocComment) {
return CommentProperties.KIND_JAVADOC;
}
return comment instanceof BlockComment ? CommentProperties.KIND_BLOCK : CommentProperties.KIND_LINE;
}
/**
* The handful of facts that differ between the four kinds of Java type declaration, resolved once
* so the ingest loop below can treat them uniformly.

View File

@@ -0,0 +1,150 @@
package com.agenticcode.parserjava;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Item 253: the OpenAPI spec of a spec-first Spring server. openapi-generator ({@code spring},
* {@code delegatePattern}) writes the controllers with their {@code @RequestMapping}s from
* {@code src/main/resources/swagger.yaml} at build time; the sources only hold the classes that
* implement the generated {@code <Tag>ApiDelegate}. So the routing lives in the spec, and this parser
* reads it: one {@code MODULE} (moduleKind {@code OPENAPI_SPEC}) and one {@code FUNCTION} per operation
* with {@code specPath} (server path + path), {@code specMethod}, {@code operationId}, {@code tag}, and the
* names the generator derives — {@code apiDelegate} ({@code i18n} → {@code I18nApiDelegate}) and
* {@code delegateMethod} ({@code ArchiveOrders} → {@code archiveOrders}). A finalize step hands path and verb
* to the implementing method. The operation itself deliberately carries no {@code httpMethod}: it is not
* the handler.
*/
public final class OpenApiSpecParser {
public static final String OPENAPI_SPEC_KIND = "OPENAPI_SPEC";
private static final List<String> VERBS = List.of("get", "put", "post", "delete", "options", "head", "patch", "trace");
private static final ObjectMapper YAML = new ObjectMapper(new YAMLFactory());
private static final ObjectMapper JSON = new ObjectMapper();
private OpenApiSpecParser() {
}
/**
* @return {@code true} when {@code head} (the start of a {@code .yaml}/{@code .yml}/{@code .json} file)
* declares an OpenAPI 3 or Swagger 2 document at the top level
*/
public static boolean looksLikeSpec(String head) {
return head.lines().anyMatch(l -> l.startsWith("openapi:") || l.startsWith("swagger:")
|| l.matches("\\s*\"(openapi|swagger)\"\\s*:.*"));
}
public static ParseResult parse(String sourceFile, String content) {
List<AstNode> nodes = new ArrayList<>();
List<AstEdge> edges = new ArrayList<>();
JsonNode doc;
try {
doc = (sourceFile.toLowerCase(Locale.ROOT).endsWith(".json") ? JSON : YAML).readTree(content);
} catch (IOException | RuntimeException e) {
return new ParseResult(nodes, edges);
}
if (doc == null || !doc.path("paths").isObject()) {
return new ParseResult(nodes, edges);
}
String server = serverPath(doc);
String[] lines = content.split("\n", -1);
Map<String, String> moduleProps = new LinkedHashMap<>();
moduleProps.put("moduleKind", OPENAPI_SPEC_KIND);
if (!doc.path("info").path("title").asText().isEmpty()) {
moduleProps.put("title", doc.path("info").path("title").asText());
}
moduleProps.put("serverPath", server);
AstNode module = new AstNode(UUID.randomUUID(), NodeType.MODULE, sourceFile, sourceFile, ConfigFileParser.LANGUAGE,
1, Math.max(1, lines.length), null, null, moduleProps);
nodes.add(module);
Iterator<Map.Entry<String, JsonNode>> paths = doc.path("paths").fields();
while (paths.hasNext()) {
Map.Entry<String, JsonNode> path = paths.next();
for (String verb : VERBS) {
JsonNode op = path.getValue().path(verb);
String operationId = op.path("operationId").asText();
if (!op.isObject() || operationId.isEmpty()) {
continue;
}
String tag = op.path("tags").path(0).asText("Default");
int line = lineOf(lines, operationId);
Map<String, String> props = new LinkedHashMap<>();
props.put("kind", "operation");
props.put("specPath", server + path.getKey());
props.put("specMethod", verb.toUpperCase(Locale.ROOT));
props.put("operationId", operationId);
props.put("tag", tag);
props.put("apiDelegate", className(tag) + "ApiDelegate");
props.put("delegateMethod", methodName(operationId));
AstNode fn = new AstNode(UUID.randomUUID(), NodeType.FUNCTION, operationId, sourceFile,
ConfigFileParser.LANGUAGE, line, line, null, null, props);
nodes.add(fn);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CONTAINS, module.id(), fn.id(), line, null));
}
}
return new ParseResult(nodes, edges);
}
/**
* The path part of {@code servers[0].url} (OpenAPI 3) or {@code basePath} (Swagger 2), without a
* trailing slash — the prefix the generator puts on every mapping ({@code /K3Koala}).
*/
static String serverPath(JsonNode doc) {
String url = doc.path("servers").path(0).path("url").asText(doc.path("basePath").asText(""));
String path = url.replaceFirst("^[A-Za-z][A-Za-z0-9+.-]*://[^/]*", "");
return path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
}
/**
* openapi-generator's class name for a tag: words split at anything not a letter or digit, each
* capitalised ({@code i18n} → {@code I18n}, {@code kasko deckung} → {@code KaskoDeckung}).
*/
static String className(String tag) {
String name = Arrays.stream(tag.split("[^A-Za-z0-9]+")).filter(w -> !w.isEmpty())
.map(w -> Character.toUpperCase(w.charAt(0)) + w.substring(1)).collect(Collectors.joining());
return name.isEmpty() ? "Default" : name;
}
/**
* openapi-generator's method name for an operationId: camel-cased at non-identifier characters, first
* letter lower-cased ({@code ArchiveOrders} → {@code archiveOrders}, {@code SVBListeX} → {@code sVBListeX}).
*/
static String methodName(String operationId) {
String[] parts = Arrays.stream(operationId.split("[^A-Za-z0-9_$]+")).filter(w -> !w.isEmpty()).toArray(String[]::new);
StringBuilder id = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
id.append(i == 0 ? parts[i] : Character.toUpperCase(parts[i].charAt(0)) + parts[i].substring(1));
}
if (id.isEmpty()) {
return "_";
}
id.setCharAt(0, Character.toLowerCase(id.charAt(0)));
return id.toString();
}
private static int lineOf(String[] lines, String operationId) {
for (int i = 0; i < lines.length; i++) {
if (lines[i].contains("operationId") && lines[i].contains(operationId)) {
return i + 1;
}
}
return 1;
}
}

View File

@@ -0,0 +1,401 @@
package com.agenticcode.parserjava;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import org.jspecify.annotations.Nullable;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Item 225: the SOAP contracts of an application as graph data.
*
* <ul>
* <li><b>Axis2 {@code services.xml}</b> (KORIN: 111): each {@code <service>} is a {@code MODULE}
* ({@code moduleKind: SOAP_SERVICE}, named by the service) with one {@code FUNCTION} per
* {@code <operation>} and a {@code REFERENCES} edge to its {@code ServiceClass} — a placeholder
* named by the class's FQN, which binds to the skeleton, whose item-214 counterpart is the
* Natural facade.</li>
* <li><b>WSDL</b> (K3: 223, KORIN: 120): each {@code <wsdl:service>} is a {@code MODULE}
* ({@code moduleKind: WSDL_SERVICE}) named {@code <file>#<service>} — the same service is
* described by several files, and the name must stay a unique identity — with the service name as
* {@code simpleName}, the {@code soap:address} as {@code location}, and one {@code FUNCTION} per
* operation of the port types its ports are bound to.</li>
* </ul>
*/
public final class SoapDescriptorParser {
public static final String SOAP_SERVICE_KIND = "SOAP_SERVICE";
public static final String WSDL_SERVICE_KIND = "WSDL_SERVICE";
/**
* Item 239: a JAX-WS {@code wsimport} binding file ({@code WS_Leitwaehrung-binding.xml}) — which Java
* package the client generated from a WSDL lives in.
*/
public static final String WSIMPORT_BINDING_KIND = "WSIMPORT_BINDING";
private SoapDescriptorParser() {
}
public static boolean isWsdl(String sourceFile) {
return sourceFile.toLowerCase(java.util.Locale.ROOT).endsWith(".wsdl");
}
public static ParseResult parse(String sourceFile, String content) {
return isWsdl(sourceFile) ? parseWsdl(sourceFile, content) : parseServicesXml(sourceFile, content);
}
// ---------------------------------------------------------------- Axis2 services.xml
static ParseResult parseServicesXml(String sourceFile, String content) {
List<AstNode> nodes = new ArrayList<>();
List<AstEdge> edges = new ArrayList<>();
List<Service> services = new ArrayList<>();
read(content, new DefaultHandler() {
private @Nullable Locator locator;
private @Nullable Service current;
private boolean serviceClass;
private boolean actionMapping;
private final StringBuilder text = new StringBuilder();
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
String element = localName.isEmpty() ? qName : localName;
Map<String, String> a = ConfigFileParser.attributes(attributes);
int line = locator == null ? 1 : locator.getLineNumber();
if (element.equals("service") && a.containsKey("name")) {
current = new Service(a.get("name"), line);
services.add(current);
} else if (current != null && element.equals("operation") && a.containsKey("name")) {
current.operations.put(a.get("name"), line);
} else if (current != null && element.equals("parameter") && "ServiceClass".equals(a.get("name"))) {
serviceClass = true;
text.setLength(0);
} else if (current != null && (element.equals("actionMapping") || element.equals("outputActionMapping"))) {
actionMapping = true;
text.setLength(0);
}
}
@Override
public void characters(char[] ch, int start, int length) {
if (serviceClass || actionMapping) {
text.append(ch, start, length);
}
}
@Override
public void endElement(String uri, String localName, String qName) {
String element = localName.isEmpty() ? qName : localName;
if (serviceClass && element.equals("parameter") && current != null) {
current.implementation = text.toString().strip();
serviceClass = false;
} else if (actionMapping && (element.equals("actionMapping") || element.equals("outputActionMapping"))) {
Matcher m = ACTION_PORT.matcher(text);
if (current != null && current.port == null && m.find()) {
current.port = m.group(1);
}
actionMapping = false;
} else if (element.equals("service")) {
current = null;
}
}
});
int lineCount = Math.max(1, (int) content.lines().count());
for (Service service : services) {
Map<String, String> props = new LinkedHashMap<>();
props.put("moduleKind", SOAP_SERVICE_KIND);
props.put("simpleName", service.name);
if (service.port != null) {
props.put("port", service.port);
}
AstNode module = new AstNode(UUID.randomUUID(), NodeType.MODULE, service.name, sourceFile,
ConfigFileParser.LANGUAGE, service.line, lineCount, null, null, props);
nodes.add(module);
operations(service, module, sourceFile, nodes, edges);
if (service.implementation != null && !service.implementation.isEmpty()) {
AstNode placeholder = new AstNode(UUID.randomUUID(), NodeType.MODULE, service.implementation, "",
"java", service.line, service.line, null, null, null);
nodes.add(placeholder);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.REFERENCES, module.id(), placeholder.id(),
service.line, null, Map.of("refKind", "SERVICE_CLASS")));
}
}
return new ParseResult(nodes, edges);
}
// ---------------------------------------------------------------- JAX-WS wsimport binding (item 239)
/**
* {@code <bindings wsdlLocation="WS_X.wsdl" xmlns="http://java.sun.com/xml/ns/jaxws"><package name="a.b"/>}
* → one {@code MODULE} (moduleKind {@code WSIMPORT_BINDING}) with {@code clientPackage} and
* {@code wsdlFile}, the WSDL's path resolved against the binding file's directory. The generated client
* is not in the sources (it is built into {@code target/}); this is the link from its package to the WSDL.
*/
public static ParseResult parseJaxwsBinding(String sourceFile, String content) {
String[] found = new String[2]; // wsdlLocation, package
read(content, new DefaultHandler() {
private int depth;
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
String element = localName.isEmpty() ? qName : localName;
element = element.substring(element.indexOf(':') + 1);
Map<String, String> a = ConfigFileParser.attributes(attributes);
if (depth == 0 && element.equals("bindings") && a.containsKey("wsdlLocation")) {
found[0] = a.get("wsdlLocation");
} else if (element.equals("package") && a.containsKey("name") && found[1] == null) {
found[1] = a.get("name");
}
depth++;
}
@Override
public void endElement(String uri, String localName, String qName) {
depth--;
}
});
List<AstNode> nodes = new ArrayList<>();
if (found[0] == null || found[1] == null) {
return new ParseResult(nodes, List.of());
}
java.nio.file.Path dir = java.nio.file.Path.of(sourceFile).getParent();
String wsdlFile = (dir == null ? java.nio.file.Path.of(found[0]) : dir.resolve(found[0]))
.normalize().toString().replace('\\', '/');
Map<String, String> props = new LinkedHashMap<>();
props.put("moduleKind", WSIMPORT_BINDING_KIND);
props.put("clientPackage", found[1]);
props.put("wsdlFile", wsdlFile);
nodes.add(new AstNode(UUID.randomUUID(), NodeType.MODULE, sourceFile, sourceFile, ConfigFileParser.LANGUAGE,
1, Math.max(1, (int) content.lines().count()), null, null, props));
return new ParseResult(nodes, List.of());
}
// ---------------------------------------------------------------- WSDL
static ParseResult parseWsdl(String sourceFile, String content) {
List<AstNode> nodes = new ArrayList<>();
List<AstEdge> edges = new ArrayList<>();
Map<String, Map<String, Integer>> portTypeOperations = new LinkedHashMap<>();
Map<String, String> bindingPortType = new LinkedHashMap<>();
List<Service> services = new ArrayList<>();
java.util.Set<String> typesPackages = new java.util.LinkedHashSet<>();
read(content, new DefaultHandler() {
private @Nullable Locator locator;
private @Nullable String portType;
private @Nullable Service service;
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
String element = localName.isEmpty() ? qName : localName;
Map<String, String> a = ConfigFileParser.attributes(attributes);
int line = locator == null ? 1 : locator.getLineNumber();
switch (element) {
case "portType" -> {
portType = a.get("name");
if (portType != null) {
portTypeOperations.putIfAbsent(portType, new LinkedHashMap<>());
}
}
case "operation" -> {
// Only a portType's operations: a binding repeats them, and outside a portType
// `portType` is null.
if (portType != null && a.containsKey("name")) {
portTypeOperations.computeIfAbsent(portType, k -> new LinkedHashMap<>())
.putIfAbsent(a.get("name"), line);
}
}
case "binding" -> {
// wsdl:binding has a name and a type; soap:binding inside it has neither.
if (a.containsKey("name") && a.containsKey("type")) {
bindingPortType.put(a.get("name"), localPart(a.get("type")));
}
}
case "service" -> {
if (a.containsKey("name")) {
service = new Service(a.get("name"), line);
services.add(service);
}
}
case "port" -> {
if (service != null && a.containsKey("binding")) {
service.bindings.add(localPart(a.get("binding")));
}
}
case "address" -> {
if (service != null && a.containsKey("location") && service.location == null) {
service.location = a.get("location");
}
}
// Item 258: the JAXB classes of an embedded schema land in the package its namespace
// maps to (urn:com-softwareag-entirex-rpc:WSK369 -> rpc.entirex.softwareag.com.wsk369)
case "schema" -> {
String pkg = a.containsKey("targetNamespace") ? jaxbPackage(a.get("targetNamespace")) : null;
if (pkg != null) {
typesPackages.add(pkg);
}
}
default -> {
}
}
}
@Override
public void endElement(String uri, String localName, String qName) {
String element = localName.isEmpty() ? qName : localName;
if (element.equals("portType")) {
portType = null;
} else if (element.equals("service")) {
service = null;
}
}
});
int lineCount = Math.max(1, (int) content.lines().count());
for (Service s : services) {
for (String binding : s.bindings) {
String type = bindingPortType.get(binding);
if (type != null && s.port == null) {
s.port = portName(type);
}
Map<String, Integer> ops = type == null ? null : portTypeOperations.get(type);
if (ops != null) {
ops.forEach(s.operations::putIfAbsent);
}
}
Map<String, String> props = new LinkedHashMap<>();
props.put("moduleKind", WSDL_SERVICE_KIND);
props.put("simpleName", s.name);
if (s.location != null) {
props.put("location", s.location);
}
if (s.port != null) {
props.put("port", s.port);
}
if (!typesPackages.isEmpty()) {
props.put("typesPackages", String.join(",", typesPackages));
}
AstNode module = new AstNode(UUID.randomUUID(), NodeType.MODULE, sourceFile + "#" + s.name, sourceFile,
ConfigFileParser.LANGUAGE, s.line, lineCount, null, null, props);
nodes.add(module);
operations(s, module, sourceFile, nodes, edges);
}
return new ParseResult(nodes, edges);
}
private static void operations(Service service, AstNode module, String sourceFile, List<AstNode> nodes,
List<AstEdge> edges) {
service.operations.forEach((name, line) -> {
AstNode operation = new AstNode(UUID.randomUUID(), NodeType.FUNCTION, name, sourceFile,
ConfigFileParser.LANGUAGE, line, line, null, null, Map.of("soapOperation", "true"));
nodes.add(operation);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CONTAINS, module.id(), operation.id(), line, null));
});
}
/**
* Item 238: the EntireX port a service exposes — {@code WSK369} in an Axis2 action mapping
* {@code http://namespace.softwareag.com/entirex/xml/mapping/WSK369Port/getDataResponse}.
*/
private static final Pattern ACTION_PORT = Pattern.compile("/([A-Za-z0-9_]+)Port/");
/**
* Item 258: the Java package JAXB derives from a namespace URI (JAXB 2.x, appendix D.5.1): scheme
* dropped, split at {@code /} and {@code :}, a URN's first part with dashes read as dots, the host
* part reversed without a leading {@code www}, everything lower-cased, characters no identifier may
* hold replaced by {@code _}. {@code http://namespace.softwareag.com/entirex/xml/mapping} →
* {@code com.softwareag.namespace.entirex.xml.mapping}. A binding customisation that names another
* package is not read. {@code null} for an empty namespace.
*/
static @Nullable String jaxbPackage(String namespace) {
String ns = namespace.trim();
boolean urn = ns.regionMatches(true, 0, "urn:", 0, 4);
ns = ns.replaceFirst("^[A-Za-z][A-Za-z0-9+.-]*:(//)?", "");
ns = ns.replaceFirst("\\.(xsd|wsdl|xml|html?)$", "");
List<String> tokens = new ArrayList<>();
for (String token : ns.split("[/:]")) {
if (!token.isEmpty()) {
tokens.add(token);
}
}
if (tokens.isEmpty()) {
return null;
}
String host = urn ? tokens.getFirst().replace('-', '.') : tokens.getFirst();
List<String> hostParts = new ArrayList<>(java.util.Arrays.asList(host.split("\\.")));
if (!hostParts.isEmpty() && hostParts.getFirst().equalsIgnoreCase("www")) {
hostParts.removeFirst();
}
java.util.Collections.reverse(hostParts);
List<String> parts = new ArrayList<>(hostParts);
parts.addAll(tokens.subList(1, tokens.size()));
List<String> segments = new ArrayList<>();
for (String part : parts) {
String segment = part.toLowerCase(java.util.Locale.ROOT).replaceAll("[^a-z0-9_]", "_");
if (segment.isEmpty()) {
continue;
}
segments.add(Character.isDigit(segment.charAt(0)) ? "_" + segment : segment);
}
return segments.isEmpty() ? null : String.join(".", segments);
}
/**
* Item 238: {@code WSK369Port} → {@code WSK369}; a portType without the suffix is the port itself.
*/
static String portName(String portType) {
return portType.endsWith("Port") && portType.length() > 4
? portType.substring(0, portType.length() - 4) : portType;
}
private static String localPart(String qualified) {
int colon = qualified.indexOf(':');
return colon < 0 ? qualified : qualified.substring(colon + 1);
}
private static void read(String content, DefaultHandler handler) {
try {
ConfigFileParser.newSaxParser().parse(new InputSource(new StringReader(content)), handler);
} catch (SAXException | IOException e) {
// Keep what was read before the error.
}
}
private static final class Service {
private final String name;
private final int line;
private final Map<String, Integer> operations = new LinkedHashMap<>();
private final List<String> bindings = new ArrayList<>();
private @Nullable String implementation;
private @Nullable String location;
private @Nullable String port;
Service(String name, int line) {
this.name = name;
this.line = line;
}
}
}

View File

@@ -0,0 +1,191 @@
package com.agenticcode.parserjava;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* Items 225/226: configuration entries (with secrets masked), Axis2 {@code services.xml} and WSDL.
*/
class ConfigAndSoapParserTest {
private static AstNode node(ParseResult result, NodeType type, String name) {
return result.nodes().stream().filter(n -> n.type() == type && n.name().equals(name)).findFirst()
.orElseThrow(() -> new AssertionError("no " + type + " " + name + " among "
+ result.nodes().stream().filter(n -> n.type() == type).map(AstNode::name).toList()));
}
private static @Nullable String prop(AstNode node, String key) {
Map<String, String> properties = node.properties();
assertNotNull(properties);
return properties.get(key);
}
private static String value(AstNode node) {
String value = node.value();
assertNotNull(value, "entry " + node.name() + " has a value");
return value;
}
@Test
void propertiesBecomeEntriesWithSecretsMasked() {
ParseResult result = ConfigFileParser.parse("res/natparm/localkos.properties", """
# comment
default.DatabaseClassname=com.microsoft.sqlserver.jdbc.SQLServerDataSource
default.Database.1=URL=jdbc:sqlserver://attskosdb1:1433;databaseName=KOS4;password=geheim;x=1
default.Database.3=Password=IWdataGRA14!
db.password = hunter2
multi.line = a\\
b
""");
AstNode file = node(result, NodeType.MODULE, "res/natparm/localkos.properties");
assertEquals("CONFIG", prop(file, "moduleKind"));
assertEquals("com.microsoft.sqlserver.jdbc.SQLServerDataSource",
node(result, NodeType.CONFIG_ENTRY, "default.DatabaseClassname").value());
String url = value(node(result, NodeType.CONFIG_ENTRY, "default.Database.1"));
assertTrue(url.contains("databaseName=KOS4"), "the database name stays searchable");
assertTrue(url.contains("password=***"), "an embedded password is masked: " + url);
assertFalse(url.contains("geheim"));
assertEquals("Password=***", node(result, NodeType.CONFIG_ENTRY, "default.Database.3").value());
assertEquals("***", node(result, NodeType.CONFIG_ENTRY, "db.password").value());
assertEquals("ab", node(result, NodeType.CONFIG_ENTRY, "multi.line").value());
assertEquals(3, node(result, NodeType.CONFIG_ENTRY, "default.DatabaseClassname").startLine() + 1);
}
@Test
void xmlIsFlattenedByPathWithNameDiscriminators() {
ParseResult result = ConfigFileParser.parse("META-INF/persistence.xml", """
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.0">
<persistence-unit name="KOS">
<jta-data-source>java:/KOSDB</jta-data-source>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect"/>
<property name="javax.persistence.jdbc.password" value="secret!"/>
</properties>
</persistence-unit>
</persistence>
""");
assertEquals("java:/KOSDB",
node(result, NodeType.CONFIG_ENTRY, "persistence/persistence-unit[KOS]/jta-data-source").value());
AstNode dialect = node(result, NodeType.CONFIG_ENTRY,
"persistence/persistence-unit[KOS]/properties/property[hibernate.dialect]@value");
assertEquals("org.hibernate.dialect.SQLServerDialect", dialect.value());
assertEquals(6, dialect.startLine());
assertEquals("***", node(result, NodeType.CONFIG_ENTRY,
"persistence/persistence-unit[KOS]/properties/property[javax.persistence.jdbc.password]@value").value());
}
@Test
void aDoctypeIsRefusedNotResolved() {
ParseResult result = ConfigFileParser.parse("x.xml", """
<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY e SYSTEM "file:///etc/passwd">]>
<r>&e;</r>
""");
assertTrue(result.nodes().stream().noneMatch(n -> n.type() == NodeType.CONFIG_ENTRY),
"an XML with a DTD yields no entries, and nothing from the entity");
}
@Test
void axis2ServiceHasOperationsAndReferencesItsSkeleton() {
ParseResult result = SoapDescriptorParser.parse("WebContent/WEB-INF/services/WS_KorinCheck/META-INF/services.xml", """
<?xml version="1.0" encoding="UTF-8"?>
<serviceGroup>
<service name="WS_KorinCheck">
<parameter name="ServiceClass">com.softwareag.namespace.entirex.xml.mapping.WS_KorinCheckSkeleton</parameter>
<operation name="getData" mep="http://www.w3.org/ns/wsdl/in-out"/>
<operation name="updateData"/>
</service>
</serviceGroup>
""");
AstNode service = node(result, NodeType.MODULE, "WS_KorinCheck");
assertEquals("SOAP_SERVICE", prop(service, "moduleKind"));
List<String> operations = result.edges().stream()
.filter(e -> e.type() == EdgeType.CONTAINS && e.sourceId().equals(service.id()))
.map(e -> result.nodes().stream().filter(n -> n.id().equals(e.targetId())).findFirst().orElseThrow().name())
.toList();
assertEquals(List.of("getData", "updateData"), operations);
AstEdge reference = result.edges().stream().filter(e -> e.type() == EdgeType.REFERENCES).findFirst().orElseThrow();
assertEquals("com.softwareag.namespace.entirex.xml.mapping.WS_KorinCheckSkeleton",
result.nodes().stream().filter(n -> n.id().equals(reference.targetId())).findFirst().orElseThrow().name());
}
@Test
void wsdlServiceHasItsAddressAndTheBoundOperations() {
ParseResult result = SoapDescriptorParser.parse("src/main/resources/wsdl/korin/WS_Leitwaehrung.wsdl", """
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="urn:x">
<wsdl:portType name="WSK304Port">
<wsdl:operation name="getData"><wsdl:input message="tns:in"/></wsdl:operation>
</wsdl:portType>
<wsdl:binding name="WSK304Binding" type="tns:WSK304Port">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="getData"><soap:operation soapAction="getData"/></wsdl:operation>
</wsdl:binding>
<wsdl:service name="WS_Leitwaehrung">
<wsdl:port name="WSK304Port" binding="tns:WSK304Binding">
<soap:address location="http://korin:8080/services/WS_Leitwaehrung"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
""");
AstNode service = node(result, NodeType.MODULE, "src/main/resources/wsdl/korin/WS_Leitwaehrung.wsdl#WS_Leitwaehrung");
assertEquals("WSDL_SERVICE", prop(service, "moduleKind"));
assertEquals("WS_Leitwaehrung", prop(service, "simpleName"));
assertEquals("http://korin:8080/services/WS_Leitwaehrung", prop(service, "location"));
assertEquals(1, result.nodes().stream().filter(n -> n.type() == NodeType.FUNCTION).count());
assertEquals("getData", node(result, NodeType.FUNCTION, "getData").name());
}
/**
* Item 258: the JAXB package of a namespace — checked against a name the corpus confirms: the korin-ws
* skeleton {@code com.softwareag.namespace.entirex.xml.mapping.WS_KontoSkeleton} is generated for the
* namespace {@code http://namespace.softwareag.com/entirex/xml/mapping}.
*/
@Test
void jaxbDerivesThePackageFromTheNamespace() {
assertEquals("com.softwareag.namespace.entirex.xml.mapping",
SoapDescriptorParser.jaxbPackage("http://namespace.softwareag.com/entirex/xml/mapping"));
assertEquals("rpc.entirex.softwareag.com.wsk369",
SoapDescriptorParser.jaxbPackage("urn:com-softwareag-entirex-rpc:WSK369"));
assertEquals("com.example.types.v1",
SoapDescriptorParser.jaxbPackage("https://www.example.com/types/v1.xsd"));
assertNull(SoapDescriptorParser.jaxbPackage(""));
}
@Test
void aWsdlNamesThePackagesOfItsSchemaTypes() {
ParseResult result = SoapDescriptorParser.parse("src/main/resources/wsdl/korinKonto/WS_Konto.wsdl", """
<?xml version="1.0" encoding="UTF-8"?>
<definitions name="WS_Konto" targetNamespace="http://namespace.softwareag.com/entirex/xml/mapping"
xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://namespace.softwareag.com/entirex/xml/mapping">
<types>
<xsd:schema targetNamespace="urn:com-softwareag-entirex-rpc:WSK369"/>
</types>
<portType name="WSK369Port"><operation name="getData"/></portType>
<binding name="WSK369Binding" type="tns:WSK369Port"/>
<service name="WS_Konto"><port name="WSK369Port" binding="tns:WSK369Binding"/></service>
</definitions>
""");
AstNode service = node(result, NodeType.MODULE, "src/main/resources/wsdl/korinKonto/WS_Konto.wsdl#WS_Konto");
assertEquals("rpc.entirex.softwareag.com.wsk369", prop(service, "typesPackages"));
assertEquals("WSK369", prop(service, "port"));
}
}

View File

@@ -0,0 +1,84 @@
package com.agenticcode.parserjava;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.*;
/**
* Item 253: an OpenAPI spec is a module of operations, named the way openapi-generator names the
* delegate interface and method.
*/
class OpenApiSpecParserTest {
private static final String SPEC = """
openapi: "3.0.3"
info:
title: K3Koala
servers:
- url: http://host:8080/K3Koala/
paths:
/api/i18n/translations:
get:
operationId: getTranslations
tags:
- i18n
/api/v1/orders/archive:
post:
operationId: ArchiveOrders
tags:
- kasko deckung
put:
summary: no operationId, not an operation the generator can name
""";
private static Map<String, String> props(ParseResult result, String name) {
AstNode node = result.nodes().stream().filter(n -> n.name().equals(name)).findFirst().orElseThrow();
return Objects.requireNonNull(node.properties());
}
@Test
void operationsCarryPathVerbAndTheGeneratedNames() {
ParseResult result = OpenApiSpecParser.parse("K3_Koala/src/main/resources/swagger.yaml", SPEC);
assertEquals(3, result.nodes().size(), "the spec module and two operations");
assertEquals(2, result.edges().size());
AstNode module = result.nodes().getFirst();
assertEquals(NodeType.MODULE, module.type());
assertEquals("OPENAPI_SPEC", Objects.requireNonNull(module.properties()).get("moduleKind"));
Map<String, String> tr = props(result, "getTranslations");
assertEquals("/K3Koala/api/i18n/translations", tr.get("specPath"));
assertEquals("GET", tr.get("specMethod"));
assertEquals("I18nApiDelegate", tr.get("apiDelegate"));
assertEquals("getTranslations", tr.get("delegateMethod"));
assertNull(tr.get("httpMethod"), "the operation is not the handler");
assertEquals(9, result.nodes().stream().filter(n -> n.name().equals("getTranslations")).findFirst().orElseThrow().startLine());
Map<String, String> archive = props(result, "ArchiveOrders");
assertEquals("KaskoDeckungApiDelegate", archive.get("apiDelegate"));
assertEquals("archiveOrders", archive.get("delegateMethod"));
assertEquals("POST", archive.get("specMethod"));
}
@Test
void swaggerTwoJsonAndNonSpecs() {
ParseResult json = OpenApiSpecParser.parse("x/src/main/resources/api.json", """
{"swagger": "2.0", "basePath": "/v2", "paths": {"/pets": {"get": {"operationId": "list-pets", "tags": ["pets"]}}}}
""");
Map<String, String> pets = props(json, "list-pets");
assertEquals("/v2/pets", pets.get("specPath"));
assertEquals("listPets", pets.get("delegateMethod"));
assertEquals("PetsApiDelegate", pets.get("apiDelegate"));
assertTrue(OpenApiSpecParser.looksLikeSpec("openapi: 3.0.3\ninfo:\n"));
assertTrue(OpenApiSpecParser.looksLikeSpec("{\n \"swagger\": \"2.0\",\n"));
assertFalse(OpenApiSpecParser.looksLikeSpec("spring:\n datasource:\n"));
assertTrue(OpenApiSpecParser.parse("a/src/main/resources/application.yaml", "spring:\n x: 1\n").nodes().isEmpty());
assertTrue(OpenApiSpecParser.parse("a/src/main/resources/broken.yaml", "openapi: [\n").nodes().isEmpty());
}
}

63
ac-parser-natjav/pom.xml Normal file
View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.agenticcode</groupId>
<artifactId>agenticcode</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>ac-parser-natjav</artifactId>
<packaging>jar</packaging>
<name>ac-parser-natjav</name>
<description>Parser for NatJav — Software AG Natural transpiled to Java (innoWake maxenso)</description>
<dependencies>
<dependency>
<groupId>com.agenticcode</groupId>
<artifactId>ac-parser-core</artifactId>
</dependency>
<!-- NatJav is syntactically Java, so the syntax layer is JavaParser; only the semantic
mapping (Natural module kinds, names, data areas) lives in this module. -->
<dependency>
<groupId>com.agenticcode</groupId>
<artifactId>ac-parser-java</artifactId>
</dependency>
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-core</artifactId>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.agenticcode</groupId>
<artifactId>ac-mvn-plugins</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,238 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.CallKind;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* The call graph of a NatJav module: its internal subroutines and every {@code perform} /
* {@code callnat} / {@code fetch} / {@code fetchReturn} they make.
*
* <p>Resolution needs no name derivation, because both sides of a call use the same Java identifier:
* {@code perform("FX_LEEREN")} names the method {@code void FX_LEEREN()}, and
* {@code callnat("V_MIT04")} names the class {@code V_MIT04}. Measured over the corpus, 93.8 % of the
* 129,590 {@code perform}s hit a method in their own file — exact and file-local. The remaining
* 6.2 % are Natural <em>external</em> subroutines, which the transpiler made classes of their own
* ({@code NaturalSubroutine}); 98.4 % of them exist as such a class, so NatJav resolves a call the
* Natural parser explicitly cannot (see the roadmap's item-71 retraction).
*
* <p>A subroutine is a parameterless method that does <b>not</b> override a runtime hook.
* {@code run}, {@code atTopOfPage}, {@code atWriteTitle}, {@code onError}, {@code atEndOfPage} and
* {@code doBreak} are the transpiled forms of Natural's inline event blocks ({@code AT TOP OF PAGE},
* {@code ON ERROR}, …), which the Natural parser attributes to the module — only
* {@code DEFINE SUBROUTINE} makes a {@code FUNCTION} there. Treating them as subroutines would invent
* ~5,300 of them and add a hop to every call inside one.
*/
final class NatJavCalls {
/**
* The Natural statements the transpiler emits as bare API calls, mapped to the call kind the
* unified AST records.
*/
private static final Map<String, CallKind> CALL_STATEMENTS = Map.of(
"perform", CallKind.PERFORM,
"callnat", CallKind.CALLNAT,
// Item 206: FETCH and FETCH RETURN used to be recorded as CALLNAT, and INPUT USING MAP and
// STACK TOP COMMAND not at all — 1,578 map displays and 289 stacked programs had no caller.
"fetch", CallKind.FETCH,
"fetchReturn", CallKind.FETCH_RETURN,
"inputUsingMap", CallKind.INPUT_MAP,
"stackCommand", CallKind.STACK_COMMAND);
/**
* Item 206: {@code stackCommand(boolean, target, …)} — the leading boolean is the runtime's
* TOP/BOTTOM flag, so the target is the second argument. Every other statement names its target
* first.
*/
private static final Map<CallKind, Integer> TARGET_ARGUMENT = Map.of(CallKind.STACK_COMMAND, 1);
/**
* Item 206: edge property naming the Natural statement behind a dynamic call that is not a plain
* {@code CALLNAT}.
*/
static final String STATEMENT = "statement";
/**
* Item 223: edge property holding the call's string-literal arguments other than the target,
* separated by {@link #LITERAL_SEPARATOR}.
*/
static final String LITERAL_ARGS = "literalArgs";
static final String LITERAL_SEPARATOR = "\u001F";
private NatJavCalls() {
}
/**
* Appends the module's {@code FUNCTION} nodes, its {@code CALLS} edges and the placeholders those
* edges point at.
*
* @return the subroutines by name, so a later pass can attribute its statements to the same
* owner this one used
*
* @param type the module's primary type
* @param module the file's shell node
* @param sourceFile the file's project-relative path
* @param copycode the file's expansion markers, used to attribute a call to the copycode it
* came from
*/
static Map<String, AstNode> collect(TypeDeclaration<?> type, AstNode module, String sourceFile,
NatJavCopycode copycode, List<AstNode> nodes, List<AstEdge> edges) {
List<MethodDeclaration> methods = type.getMethods();
// Subroutines first: a `perform` may call one declared further down the file.
Map<String, AstNode> subroutines = new LinkedHashMap<>();
for (MethodDeclaration method : methods) {
if (!isSubroutine(method)) {
continue;
}
AstNode function = new AstNode(UUID.randomUUID(), NodeType.FUNCTION, method.getNameAsString(),
sourceFile, NatJavParser.LANGUAGE, lineOf(method), endLineOf(method), null, null,
copycode.attribute(Map.of(), lineOf(method)));
nodes.add(function);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CONTAINS, module.id(), function.id(),
function.startLine(), null));
subroutines.put(function.name(), function);
}
Map<String, AstNode> placeholders = new HashMap<>();
for (MethodDeclaration method : methods) {
// Calls in an event block or in run() belong to the module, exactly as in Natural.
AstNode caller = subroutines.getOrDefault(method.getNameAsString(), module);
for (MethodCallExpr call : method.findAll(MethodCallExpr.class)) {
CallKind kind = CALL_STATEMENTS.get(call.getNameAsString());
// A bare call is the transpiled Natural statement; `x.perform(...)` would be something
// else entirely (a runtime object's own method).
if (kind == null || call.getScope().isPresent()
|| call.getArguments().size() <= TARGET_ARGUMENT.getOrDefault(kind, 0)) {
continue;
}
emitCall(call, kind, caller, subroutines, placeholders, copycode, nodes, edges);
}
}
return subroutines;
}
private static void emitCall(MethodCallExpr call, CallKind kind, AstNode caller,
Map<String, AstNode> subroutines, Map<String, AstNode> placeholders,
NatJavCopycode copycode, List<AstNode> nodes, List<AstEdge> edges) {
int lineNo = lineOf(call);
Map<String, String> properties = new LinkedHashMap<>();
Expression target = call.getArgument(TARGET_ARGUMENT.getOrDefault(kind, 0));
AstNode targetNode;
if (target instanceof StringLiteralExpr literal) {
String name = literal.asString().trim();
if (name.isEmpty()) {
return;
}
// Item 206: a stacked command with parameters ('LOGON KORIN') is a system command, not a
// module — as a call it would only mint a placeholder that can never resolve.
if (kind == CallKind.STACK_COMMAND && name.chars().anyMatch(Character::isWhitespace)) {
return;
}
AstNode local = kind == CallKind.PERFORM ? subroutines.get(name) : null;
// A local subroutine wins over a same-named module: Natural resolves PERFORM against the
// module's own subroutines first, and 93.8 % of the corpus's targets are exactly that.
targetNode = local != null ? local : placeholder(placeholders, nodes, name, lineNo, true);
properties.put("callKind", kind.name());
} else {
// Item 206: a stacked command held in a variable often carries parameters too, so its
// value is not a module name the dynamic-call resolution could bind. Left out.
if (kind == CallKind.STACK_COMMAND) {
return;
}
// Dynamic dispatch: the target name lives in a variable. Recorded as a placeholder named
// after it plus the dynamicVar marker, the same shape the Natural parser uses so the
// constant-propagation enrichment can recover literal targets later.
String variable = target.toString().trim();
targetNode = placeholder(placeholders, nodes, variable, lineNo, false);
properties.put("callKind", CallKind.CALLNAT_DYNAMIC.name());
properties.put("dynamicVar", variable);
// Item 206: the kind stays CALLNAT_DYNAMIC so the dynamic-call resolution still binds it;
// which statement it was (FETCH, FETCH RETURN, INPUT USING MAP) rides along, and the
// resolvers copy it onto the edge they build.
if (kind != CallKind.CALLNAT && kind != CallKind.PERFORM) {
properties.put(STATEMENT, kind.name());
}
}
// Item 223: the other string literals the statement passes (message numbers, keys, program
// names handed on), so search/value finds a literal that is an argument rather than a target.
List<String> literals = new ArrayList<>();
for (int i = 0; i < call.getArguments().size(); i++) {
if (i != TARGET_ARGUMENT.getOrDefault(kind, 0)
&& call.getArgument(i) instanceof StringLiteralExpr argument
&& !argument.asString().isBlank()) {
literals.add(argument.asString().trim());
}
}
if (!literals.isEmpty()) {
properties.put(LITERAL_ARGS, String.join(LITERAL_SEPARATOR, literals));
}
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CALLS, caller.id(), targetNode.id(), lineNo,
null, copycode.attribute(properties, lineNo)));
}
/**
* @return the deduped unresolved-module placeholder for {@code name}, carrying the class name the
* call site used so the NatJav placeholder resolution can key on it (the call names the Java
* class, the module node its Natural name)
*/
private static AstNode placeholder(Map<String, AstNode> placeholders, List<AstNode> nodes,
String name, int lineNo, boolean literalTarget) {
return placeholders.computeIfAbsent(name, n -> {
// className only where the name IS a class name. For a dynamic call it is the dispatch
// variable's text, and the NatJav placeholder re-keying matches any placeholder carrying
// one — so a variable that happens to share a module's identifier would have been wired
// to that module as a real call it never makes.
Map<String, String> properties = literalTarget
? Map.of(NatJavUsings.CLASS_NAME, n) : Map.of();
AstNode node = new AstNode(UUID.randomUUID(), NodeType.MODULE, n, "", NatJavParser.LANGUAGE,
lineNo, lineNo, null, null, properties);
nodes.add(node);
return node;
});
}
/**
* @return whether {@code method} is a Natural internal subroutine rather than an event block or
* the module body
*/
private static boolean isSubroutine(MethodDeclaration method) {
return method.getParameters().isEmpty()
&& !method.isAnnotationPresent("Override")
&& !RUNTIME_HOOKS.contains(method.getNameAsString());
}
/**
* The runtime hooks, checked by name as well as by {@code @Override}: a generated file may omit
* the annotation, and mistaking {@code run} for a subroutine would attach the whole module body
* to a function Natural does not have.
*/
private static final Set<String> RUNTIME_HOOKS = Set.of(
"run", "onError", "atWriteTitle", "atTopOfPage", "atEndOfPage", "doBreak",
"atStartOfData", "atEndOfData", "atBreak", "initialize");
private static int lineOf(com.github.javaparser.ast.Node node) {
return node.getBegin().map(p -> p.line).orElse(1);
}
private static int endLineOf(com.github.javaparser.ast.Node node) {
return node.getEnd().map(p -> p.line).orElse(1);
}
}

View File

@@ -0,0 +1,93 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.LocMetrics;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.model.SourceHash;
import com.agenticcode.parsercore.ast.spi.CoarseScanner;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* Tier-1 coarse scan of a NatJav file (roadmap item 36): the file's outline only — its
* {@code MODULE}/{@code DATA_STRUCTURE} shell, stamped with the content hash (item 41) and
* LoC/SLoC (item 46) — with the deep detail left to {@link NatJavParser}.
*
* <p>While the parser emits nothing beyond the shell, this scan is the same result plus the
* metrics; it filters rather than assumes, so it keeps producing the correct outline as the parser
* grows a call graph and data structures in later phases.
*/
public final class NatJavCoarseScanner implements CoarseScanner {
private final NatJavParser parser = new NatJavParser();
private final NatJavLineCounter lineCounter = new NatJavLineCounter();
/**
* @return {@code shell} with {@code sourceHash} and {@code loc}/{@code sloc} added, preserving
* every property the parser already set (naturalName identity, moduleKind, description, ...)
*/
private static AstNode withShellMetrics(AstNode shell, String hash, LocMetrics metrics) {
Map<String, String> properties = new HashMap<>();
if (shell.properties() != null) {
properties.putAll(shell.properties());
}
properties.put(SourceHash.PROPERTY, hash);
properties.put(LocMetrics.LOC, Integer.toString(metrics.loc()));
properties.put(LocMetrics.SLOC, Integer.toString(metrics.sloc()));
return new AstNode(shell.id(), shell.type(), shell.name(), shell.sourceFile(), shell.language(),
shell.startLine(), shell.endLine(), shell.dataType(), shell.value(), properties);
}
@Override
public String language() {
return NatJavParser.LANGUAGE;
}
@Override
public ParseResult scan(String sourceFile, String content) {
return scan(sourceFile, content, NatJavDdmResolver.NONE);
}
/**
* Tier-1 scan with the project's DDM index; see {@link NatJavParser#parse(String, String,
* NatJavDdmResolver)}.
*/
public ParseResult scan(String sourceFile, String content, NatJavDdmResolver ddms) {
NatJavParser.Parsed parsed = parser.parseWithShell(sourceFile, content, ddms);
String hash = SourceHash.of(content);
LocMetrics metrics = lineCounter.count(content);
// Kept: the file's shell, its FUNCTION shells (subroutines — the call graph is Tier-1) and
// the unresolved cross-file references (USING/CALLNAT placeholders, which carry an empty
// sourceFile). The field tree is deep detail a Tier-2 parse fills in — and it cannot be
// filtered by node type, because a data area's groups are DATA_STRUCTUREs as well, which is
// why the shell is recognised by identity rather than by type.
List<AstNode> coarseNodes = new ArrayList<>();
Set<UUID> kept = new HashSet<>();
for (AstNode node : parsed.result().nodes()) {
boolean shell = node.id().equals(parsed.shell().id());
// A placeholder is only outline material when it stands for another FILE — a USING or a
// CALLNAT target. Dataflow now also mints field placeholders and DB_TABLEs with an empty
// sourceFile, and keeping those made a Tier-1 scan persist the whole cross-file field
// population (340,667 on `korin`) plus tables with no access to hang off.
boolean crossFileReference = node.sourceFile().isEmpty()
&& (node.type() == NodeType.MODULE || node.type() == NodeType.DATA_STRUCTURE);
boolean outline = shell || node.type() == NodeType.FUNCTION || crossFileReference;
if (!outline) {
continue;
}
kept.add(node.id());
coarseNodes.add(shell ? withShellMetrics(node, hash, metrics) : node);
}
return new ParseResult(coarseNodes, parsed.result().edges().stream()
.filter(e -> kept.contains(e.sourceId()) && kept.contains(e.targetId()))
.toList());
}
}

View File

@@ -0,0 +1,128 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parserjava.JavaComments;
import com.github.javaparser.ast.CompilationUnit;
import java.util.List;
import java.util.regex.Pattern;
/**
* Comment blocks of a NatJav file. The grouping and the {@code DOCUMENTS} attachment are the Java
* parser's ({@link JavaComments}) — NatJav <em>is</em> Java syntax — and what this class adds is the
* one thing that differs: a transpiled file carries the transpiler's own bookkeeping alongside the
* comments a Natural developer wrote.
*
* <p>Measured over the corpus's 2,473,041 comment lines, 26 % are that bookkeeping, and every part of
* it is <b>already in the graph as structured data</b>: the {@code @@INCLUDE} markers as a call's
* {@code viaCopycode}, the header's {@code @FUNCTION}/{@code @OWNER} as the module's
* {@code description}/{@code owner}. Storing it again as comment text would bury the real comments —
* {@code search_by_value} with {@code includeComments} would answer a quarter transpiler markers.
*
* <p><b>The filtering is per line, not per block</b>, and that distinction is the whole point. The
* migration header looks like pure framing but is not: of the 14,749 files carrying one, <b>10,466
* (71 %)</b> have real prose inside it — often a fuller description of the program than the single
* {@code @FUNCTION} line. Dropping the block would delete the documentation of 10,466 modules.
*
* <pre>
* * *LCM-HEADER BEGIN ****** $$LCMHEADER:20140410-V00.01*** ← framing, dropped
* * * @FUNCTION LOESCHEN UMKONTIERUNGEN ← already the module's description
* * * @OWNER P36 ← already the module's owner
* * *
* * * Löschen Umkontierungen ← kept
* </pre>
*/
final class NatJavComments {
/**
* A block that is nothing but transpiler bookkeeping: the {@code @@USING}/{@code @@INCLUDE}
* expansion markers (245,024 lines) and the generation banner (21,603). Unlike the migration
* header these never carry prose, so they are dropped whole.
*/
private static final Pattern GENERATED_BLOCK = Pattern.compile(
"^\\s*(@@(USING|INCLUDE):(BEGIN|END)\\(|Generated by innoWake"
+ "|additional loop and database objects)");
/**
* One line of the migration header's frame, or a field of it already extracted into a module
* property. Everything else in the header is the developer's own text.
*/
private static final Pattern HEADER_FRAME = Pattern.compile(
"^[\\s*]*$"
+ "|LCM-HEADER\\s+(BEGIN|END)"
+ "|\\$\\$LCMHEADER"
// The corpus spells this both ways: 57,484 "migration" and 1,347 "migraion".
// Matching the correct spelling only let 442 blocks of pure framing through.
+ "|header created during migra[a-z]*ion process"
+ "|@(FUNCTION|OWNER|MERGE-SOURCE|NO-MERGE)\\b"
// The generator stamps per-line metadata wherever it likes, and the label is not
// one word: besides `versionRep2Str:` and `originalObjectName:` there is
// `Scanpunkte für manuelle Eingriffe:` (2,140 lines, also HTML-escaped as
// `f&uuml;r`). Any line that opens with the generator's name and carries a label
// is its stamp — a hand-written comment does not start that way.
+ "|^[\\s*]*innoWake\\b[^:]*:"
+ "|^\\s*</?pre>\\s*$");
private NatJavComments() {
}
/**
* Appends the file's comment nodes and their {@code DOCUMENTS} edges.
*/
static void collect(CompilationUnit unit, String sourceFile, List<AstNode> nodes, List<AstEdge> edges) {
JavaComments.collect(unit, NatJavParser.LANGUAGE, sourceFile, nodes, edges, NatJavComments::retained);
}
/**
* @return the part of a comment block worth keeping: {@code ""} for pure bookkeeping, otherwise
* the block without its framing lines
*/
static String retained(String text) {
if (GENERATED_BLOCK.matcher(text).find()) {
return "";
}
StringBuilder kept = new StringBuilder();
for (String line : text.split("\\R")) {
if (HEADER_FRAME.matcher(line).find() || isDecorationOnly(line)) {
continue;
}
if (kept.length() > 0) {
kept.append('\n');
}
kept.append(withoutNaturalMarker(line));
}
// Item 208: the header and much of the Javadoc prose are HTML-escaped (f&uuml;r); the comment
// text is stored decoded, as a Natural ingest stores it.
return NatJavHtml.decode(kept.toString().strip());
}
/**
* @return the line without Natural's own comment marker. The Java layer already stripped the
* Javadoc {@code *}; underneath it sits the {@code *} that marked a full-line comment in the
* Natural source, and a Natural ingest does not store that either — the sentence is wanted, not
* the comment syntax.
*/
private static String withoutNaturalMarker(String line) {
String core = line.strip();
return core.startsWith("*") ? core.substring(1).strip() : core;
}
/**
* @return whether the line is only the comment box's own drawing — a row of asterisks, dashes or
* equals signs, which the Natural sources use to frame a header and which carries no statement
*/
private static boolean isDecorationOnly(String line) {
String core = line.strip();
if (core.isEmpty()) {
return true;
}
for (int i = 0; i < core.length(); i++) {
char c = core.charAt(i);
if (c != '*' && c != '-' && c != '=' && c != ' ' && c != '/') {
return false;
}
}
return true;
}
}

Some files were not shown because too many files have changed in this diff Show More