Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba4e5961b8 | ||
|
|
c05ac33e5a | ||
|
|
3e923bed42 | ||
|
|
129987a4c1 | ||
|
|
a0e23c6594 | ||
|
|
0264bd554e | ||
|
|
0066f9f717 | ||
|
|
2207775c73 | ||
|
|
7ef7fe4a89 | ||
|
|
af189ee706 | ||
|
|
eea2cbf900 | ||
|
|
9bd6c5f3ae | ||
|
|
ccf2a9defb | ||
|
|
62abc1ed86 | ||
|
|
f54a829054 | ||
|
|
98b6549308 | ||
|
|
2b38e466b4 | ||
|
|
de5ca4428c | ||
|
|
0a9718399a | ||
|
|
cfb367a1bc | ||
|
|
52f2f4ae36 | ||
|
|
15e2a16eaf | ||
|
|
7a33dfd956 | ||
|
|
ef24c8c20a | ||
|
|
6650db68b9 | ||
|
|
281b7a4298 | ||
|
|
2b208fa9d0 | ||
|
|
f31892b345 | ||
|
|
55b978510d | ||
|
|
abcdba91dd | ||
|
|
54065cb203 | ||
|
|
ad9dcab09d | ||
|
|
0ec965b2af | ||
|
|
c8d4ff6120 | ||
|
|
edd092a176 | ||
|
|
1925d964df | ||
|
|
664eb35227 | ||
|
|
5848f32bf7 | ||
|
|
d1fbea247a | ||
|
|
9c39f8f3eb | ||
|
|
89dcf2bbaa | ||
|
|
443c213459 | ||
|
|
c054ab5e48 | ||
|
|
3f63c94469 |
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"permissions": {
|
||||
"deny": [
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)"
|
||||
],
|
||||
"ask": [
|
||||
"Bash(rm:*)",
|
||||
"Bash(rmdir:*)",
|
||||
"Bash(git:*)"
|
||||
"Bash(git:*)",
|
||||
"Bash(git commit:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
3
.gitignore
vendored
@@ -45,3 +45,6 @@ replay_pid*
|
||||
|
||||
/.mvn/
|
||||
/.idea/
|
||||
|
||||
# local symlink to external project
|
||||
/korin-natjav
|
||||
|
||||
11
CLAUDE.md
11
CLAUDE.md
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
61
ac-cli/src/main/java/com/agenticcode/cli/RefreshJobWait.java
Normal file
61
ac-cli/src/main/java/com/agenticcode/cli/RefreshJobWait.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
45
ac-cli/src/main/java/com/agenticcode/cli/ScreensCommand.java
Normal file
45
ac-cli/src/main/java/com/agenticcode/cli/ScreensCommand.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
652
ac-code-server/src/test/resources/fixtures/natjav/COMMON_DA.java
Normal file
652
ac-code-server/src/test/resources/fixtures/natjav/COMMON_DA.java
Normal 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");
|
||||
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
113
ac-code-server/src/test/resources/fixtures/natjav/V_MIT04.java
Normal file
113
ac-code-server/src/test/resources/fixtures/natjav/V_MIT04.java
Normal 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();
|
||||
}
|
||||
}
|
||||
/* * */
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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) {
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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
63
ac-parser-natjav/pom.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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ü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ü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
Reference in New Issue
Block a user