Skip to main content

zerodds_idl_java/
emitter.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! AST-Walker, der Java-17-Source-Files emittiert.
4//!
5//! Block-A: Header-Layout (`package`, `import`, Class-Modifiers).
6//! Block-B: Primitive-Mapping (delegiert an [`crate::type_map`]).
7//! Block-C: struct/enum/union/typedef/sequence/array/inheritance.
8//! Block-D: Exception → `class X extends RuntimeException`.
9//!
10//! Java erfordert eine `.java`-Datei pro top-level public class. Der
11//! Emitter sammelt waehrend des AST-Walks pro Top-Level-Type genau eine
12//! [`JavaFile`]-Struktur.
13
14use std::collections::{BTreeSet, HashMap};
15use std::fmt::Write;
16// zerodds-lint: BTreeSet wird im Emitter fuer ImportSet + Cycle-Detection verwendet.
17
18use zerodds_idl::ast::{
19    Annotation, AnnotationParams, CaseLabel, ConstExpr, ConstrTypeDecl, Declarator, Definition,
20    EnumDef, ExceptDecl, IntegerType, InterfaceDcl, InterfaceDef, Literal, LiteralKind, Member,
21    ScopedName, Specification, StructDcl, StructDef, SwitchTypeSpec, TypeDecl, TypeSpec,
22    TypedefDecl, UnionDcl, UnionDef,
23};
24
25use zerodds_idl::semantics::annotations::PlacementKind;
26
27use crate::JavaGenOptions;
28use crate::annotations::{
29    enum_value_override, has_nested, lower_or_empty, member_annotation_lines, type_annotation_lines,
30};
31use crate::bitset::{emit_bitmask_file, emit_bitset_file};
32use crate::error::JavaGenError;
33use crate::keywords::sanitize_identifier;
34use crate::type_map::{
35    floating_to_java, floating_to_java_boxed, integer_to_java, integer_to_java_boxed, is_unsigned,
36    primitive_to_java, primitive_to_java_boxed,
37};
38use crate::verbatim::emit_verbatim_at;
39
40/// Eine einzelne generierte Java-Source-Datei.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct JavaFile {
43    /// Java-Package-Pfad mit Punkt-Trennern (z.B. `org.example.types`).
44    pub package_path: String,
45    /// Class-Name = Datei-Name ohne `.java`-Suffix.
46    pub class_name: String,
47    /// Vollstaendige Source-Datei (inkl. `package`, Imports, Class-Body).
48    pub source: String,
49}
50
51impl JavaFile {
52    /// Liefert den relativen Pfad fuer die Datei (z.B. `org/example/Foo.java`).
53    #[must_use]
54    pub fn relative_path(&self) -> String {
55        let dir = self.package_path.replace('.', "/");
56        if dir.is_empty() {
57            format!("{}.java", self.class_name)
58        } else {
59            format!("{dir}/{}.java", self.class_name)
60        }
61    }
62}
63
64/// Haupt-Entry: walkt das IDL-AST und emittiert eine Liste von Java-Files.
65pub(crate) fn emit_files(
66    spec: &Specification,
67    opts: &JavaGenOptions,
68) -> Result<Vec<JavaFile>, JavaGenError> {
69    detect_inheritance_cycles(spec)?;
70
71    // Pre-pass: index every Struct-Name → its (transitive) base-chain
72    // for the Multi-Inheritance Interface-Pattern (C5.4-b §3).
73    let parent_of = collect_base_chain_index(spec);
74
75    let mut files: Vec<JavaFile> = Vec::new();
76    let pkg = sanitize_package(&opts.root_package);
77    let ctx = EmitCtx { parent_of };
78    walk_definitions(&spec.definitions, &pkg, opts, &mut files, &ctx)?;
79    Ok(files)
80}
81
82/// Emitter-Kontext, der waehrend des AST-Walks read-only gehalten wird.
83/// Hier landet der Multi-Inheritance-Index plus Spaeter weitere globale
84/// Lookup-Tables (z.B. type-name → topic-eligibility).
85#[derive(Debug, Default)]
86pub(crate) struct EmitCtx {
87    /// Mapping `struct-Name → direkter Basis-Name` (kurzform, ohne
88    /// Modul-Prefix). Wir benutzen den letzten `.`-getrennten Token
89    /// (siehe [`scoped_to_short`]).
90    pub parent_of: std::collections::HashMap<String, String>,
91}
92
93fn sanitize_package(p: &str) -> String {
94    p.trim_matches('.').to_string()
95}
96
97/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
98fn walk_definitions(
99    defs: &[Definition],
100    pkg: &str,
101    opts: &JavaGenOptions,
102    files: &mut Vec<JavaFile>,
103    ctx: &EmitCtx,
104) -> Result<(), JavaGenError> {
105    for d in defs {
106        match d {
107            Definition::Module(m) => {
108                let name = sanitize_identifier(&m.name.text)?.to_lowercase();
109                let sub_pkg = if pkg.is_empty() {
110                    name
111                } else {
112                    format!("{pkg}.{name}")
113                };
114                walk_definitions(&m.definitions, &sub_pkg, opts, files, ctx)?;
115            }
116            Definition::Type(td) => emit_type_decl_top(td, pkg, opts, files, ctx)?,
117            Definition::Const(c) => {
118                let file = emit_const_holder(c, pkg, opts)?;
119                files.push(file);
120            }
121            Definition::Except(e) => {
122                let file = emit_exception_file(e, pkg, opts)?;
123                files.push(file);
124            }
125            Definition::Interface(InterfaceDcl::Def(iface)) => {
126                if is_service_interface(iface) {
127                    emit_service_interface_files(iface, pkg, opts, files)?;
128                } else {
129                    // Spec idl4-java §7.4: IDL interface -> Java public interface.
130                    files.push(emit_non_service_interface_file(iface, pkg, opts)?);
131                }
132            }
133            Definition::Interface(InterfaceDcl::Forward(_)) => {
134                // §7.4.2: forward decl has no Java mapping.
135            }
136            Definition::ValueDef(v) => {
137                let value_files = emit_value_type_files(v, pkg, opts)?;
138                files.extend(value_files);
139            }
140            Definition::ValueBox(_) | Definition::ValueForward(_) => {
141                // ValueBox + ValueForward sind Foundation-no-op.
142            }
143            Definition::TypeId(_)
144            | Definition::TypePrefix(_)
145            | Definition::Import(_)
146            | Definition::Component(_)
147            | Definition::Home(_)
148            | Definition::Event(_)
149            | Definition::Porttype(_)
150            | Definition::Connector(_)
151            | Definition::TemplateModule(_)
152            | Definition::TemplateModuleInst(_) => {
153                return Err(JavaGenError::UnsupportedConstruct {
154                    construct: "corba/ccm/template construct".into(),
155                    context: None,
156                });
157            }
158            Definition::Annotation(_) => {
159                // §7.4.15: User-Defined Annotation-Defs werden bei
160                // Anwendung in den annotierten Mitgliedern emittiert,
161                // nicht als eigenes Top-Level-Java-Konstrukt.
162            }
163            Definition::VendorExtension(v) => {
164                return Err(JavaGenError::UnsupportedConstruct {
165                    construct: format!("vendor-extension:{}", v.production_name),
166                    context: None,
167                });
168            }
169        }
170    }
171    Ok(())
172}
173
174fn emit_type_decl_top(
175    td: &TypeDecl,
176    pkg: &str,
177    opts: &JavaGenOptions,
178    files: &mut Vec<JavaFile>,
179    ctx: &EmitCtx,
180) -> Result<(), JavaGenError> {
181    match td {
182        TypeDecl::Constr(c) => match c {
183            ConstrTypeDecl::Struct(StructDcl::Def(s)) => {
184                files.push(emit_struct_file(s, pkg, opts, ctx)?);
185                // Multi-Inheritance-Pattern: emittiere fuer jeden
186                // Struct, der selbst Basis eines anderen Struct ist,
187                // ein Companion-Interface — so kann ein Sub-Sub-Class
188                // den jeweils transitiven Vorfahren via `implements
189                // <Anc>Interface` einbinden.
190                if ctx.parent_of.values().any(|p| p == &s.name.text) {
191                    files.push(emit_struct_companion_interface(s, pkg, opts)?);
192                }
193                Ok(())
194            }
195            ConstrTypeDecl::Struct(StructDcl::Forward(_)) => {
196                // Forward-Decls in Java implizit (Class wird ohnehin
197                // separat erzeugt) — kein File noetig.
198                Ok(())
199            }
200            ConstrTypeDecl::Union(UnionDcl::Def(u)) => {
201                files.extend(emit_union_files(u, pkg, opts)?);
202                Ok(())
203            }
204            ConstrTypeDecl::Union(UnionDcl::Forward(_)) => Ok(()),
205            ConstrTypeDecl::Enum(e) => {
206                files.push(emit_enum_file(e, pkg, opts)?);
207                Ok(())
208            }
209            ConstrTypeDecl::Bitset(b) => {
210                files.push(emit_bitset_file(b, pkg, opts)?);
211                Ok(())
212            }
213            ConstrTypeDecl::Bitmask(b) => {
214                files.push(emit_bitmask_file(b, pkg, opts)?);
215                Ok(())
216            }
217        },
218        TypeDecl::Typedef(t) => {
219            files.extend(emit_typedef_files(t, pkg, opts)?);
220            Ok(())
221        }
222    }
223}
224
225// ---------------------------------------------------------------------------
226// Per-Type-Emitter (jeweils 1 JavaFile)
227// ---------------------------------------------------------------------------
228
229fn emit_struct_file(
230    s: &StructDef,
231    pkg: &str,
232    opts: &JavaGenOptions,
233    ctx: &EmitCtx,
234) -> Result<JavaFile, JavaGenError> {
235    let class = sanitize_identifier(&s.name.text)?;
236    let mut imports = ImportSet::default();
237    let ind = indent_unit(opts);
238
239    // Pre-Walk: imports sammeln.
240    for m in &s.members {
241        collect_member_imports(m, &mut imports);
242    }
243
244    let mut body = String::new();
245
246    // §7.2.2.4.8 — `@verbatim(placement=BEGIN_FILE)` direkt am Anfang
247    // des body (sitzt nach package + imports im Compilation-Unit-Wrap).
248    emit_verbatim_at(&mut body, "", &s.annotations, PlacementKind::BeginFile)?;
249
250    // §7.2.2.4.8 — `@verbatim(placement=BEFORE_DECLARATION)`.
251    emit_verbatim_at(
252        &mut body,
253        "",
254        &s.annotations,
255        PlacementKind::BeforeDeclaration,
256    )?;
257
258    // Type-level Annotations (`@Nested`, `@Extensibility(...)`).
259    for line in type_annotation_lines(&s.annotations) {
260        writeln!(body, "{line}").map_err(fmt_err)?;
261    }
262
263    let extends = if let Some(base) = &s.base {
264        let base_str = scoped_to_java(base);
265        format!(" extends {base_str}")
266    } else {
267        String::new()
268    };
269
270    // Multi-Inheritance-Pattern: alle transitiven Vorfahren *jenseits*
271    // der direkten Basis werden als `implements <X>Interface` gefuehrt.
272    // Bei einer einfachen Hierarchie (Single-Base ohne Grandparent)
273    // bleibt diese Liste leer.
274    let mut implements: Vec<String> = transitive_ancestors_beyond_base(&s.name.text, ctx)
275        .into_iter()
276        .map(|anc| format!("{anc}Interface"))
277        .collect();
278
279    // TopicType-Marker: jede top-level-Struct ohne `@nested`-Marker
280    // **und ohne Basis** implementiert `org.omg.dds.topic.TopicType<Self>`.
281    //
282    // Sub-Strukturen (`struct Child : Base`) erben den Marker vom
283    // Parent ueber die regulaere `extends`-Kette — Java verbietet die
284    // erneute Implementierung mit eigenem Generic-Param (`TopicType<Child>`
285    // vs. `TopicType<Base>`). Das ist spec-konform: in DDS-Java-PSM ist
286    // `TopicType<T>` ein Marker-Interface, dessen Generic-Param nur am
287    // Wurzel-Type der Vererbungs-Kette steht. Ein Sub-struct ist gemaess
288    // Inheritance-Regel weiterhin `instanceof TopicType<Base>` und damit
289    // als Topic-Type registrierbar.
290    //
291    // Findings-Anker: TS-3-Finding 4 (`docs/test-harness/plan.md`).
292    let lowered_type = lower_or_empty(&s.annotations);
293    if !has_nested(&lowered_type) && s.base.is_none() {
294        implements.push(format!("org.omg.dds.topic.TopicType<{class}>"));
295    }
296    let implements_clause = if implements.is_empty() {
297        String::new()
298    } else {
299        format!(" implements {}", implements.join(", "))
300    };
301
302    writeln!(body, "public class {class}{extends}{implements_clause} {{").map_err(fmt_err)?;
303
304    // §7.2.2.4.8 — `@verbatim(placement=BEGIN_DECLARATION)` als erste
305    // Zeile innerhalb des Class-Bodys.
306    emit_verbatim_at(
307        &mut body,
308        &ind,
309        &s.annotations,
310        PlacementKind::BeginDeclaration,
311    )?;
312
313    // Felder.
314    for m in &s.members {
315        emit_member_field(&mut body, m, &ind)?;
316    }
317    writeln!(body).map_err(fmt_err)?;
318
319    // Default-Konstruktor.
320    writeln!(body, "{ind}public {class}() {{}}").map_err(fmt_err)?;
321    writeln!(body).map_err(fmt_err)?;
322
323    // Bean-Style Getter / Setter.
324    for m in &s.members {
325        emit_member_accessors(&mut body, m, &ind)?;
326    }
327
328    // §7.2.2.4.8 — `@verbatim(placement=END_DECLARATION)` als letzte
329    // Zeile vor dem schliessenden `}`.
330    emit_verbatim_at(
331        &mut body,
332        &ind,
333        &s.annotations,
334        PlacementKind::EndDeclaration,
335    )?;
336
337    writeln!(body, "}}").map_err(fmt_err)?;
338
339    // §7.2.2.4.8 — `@verbatim(placement=AFTER_DECLARATION/END_FILE)`.
340    emit_verbatim_at(
341        &mut body,
342        "",
343        &s.annotations,
344        PlacementKind::AfterDeclaration,
345    )?;
346    emit_verbatim_at(&mut body, "", &s.annotations, PlacementKind::EndFile)?;
347
348    let source = wrap_compilation_unit(pkg, &imports, &body);
349    Ok(JavaFile {
350        package_path: pkg.to_string(),
351        class_name: class,
352        source,
353    })
354}
355
356fn emit_enum_file(e: &EnumDef, pkg: &str, opts: &JavaGenOptions) -> Result<JavaFile, JavaGenError> {
357    let class = sanitize_identifier(&e.name.text)?;
358    let ind = indent_unit(opts);
359    let mut body = String::new();
360
361    emit_verbatim_at(&mut body, "", &e.annotations, PlacementKind::BeginFile)?;
362    emit_verbatim_at(
363        &mut body,
364        "",
365        &e.annotations,
366        PlacementKind::BeforeDeclaration,
367    )?;
368
369    // Type-level annotations (`@Nested`, `@Extensibility(...)`).
370    for line in type_annotation_lines(&e.annotations) {
371        writeln!(body, "{line}").map_err(fmt_err)?;
372    }
373
374    writeln!(body, "public enum {class} {{").map_err(fmt_err)?;
375    emit_verbatim_at(
376        &mut body,
377        &ind,
378        &e.annotations,
379        PlacementKind::BeginDeclaration,
380    )?;
381
382    let count = e.enumerators.len();
383    let mut next_implicit: i64 = 0;
384    for (idx, en) in e.enumerators.iter().enumerate() {
385        let name = sanitize_identifier(&en.name.text)?;
386        let sep = if idx + 1 == count { ';' } else { ',' };
387        // Explicit `@value(N)` overrides the auto-assigned ordinal.
388        // Spec idl4-java-1.0 §7.2 — Custom-Werte statt Auto-Ordinals.
389        let value_lit = match enum_value_override(&en.annotations) {
390            Some(raw) => match raw.parse::<i64>() {
391                Ok(n) => {
392                    next_implicit = n + 1;
393                    n.to_string()
394                }
395                Err(_) => raw,
396            },
397            None => {
398                let n = next_implicit;
399                next_implicit += 1;
400                n.to_string()
401            }
402        };
403        writeln!(body, "{ind}{name}({value_lit}){sep}").map_err(fmt_err)?;
404    }
405    writeln!(body).map_err(fmt_err)?;
406    writeln!(body, "{ind}private final int value;").map_err(fmt_err)?;
407    writeln!(body, "{ind}{class}(int value) {{ this.value = value; }}").map_err(fmt_err)?;
408    writeln!(body, "{ind}public int value() {{ return value; }}").map_err(fmt_err)?;
409    emit_verbatim_at(
410        &mut body,
411        &ind,
412        &e.annotations,
413        PlacementKind::EndDeclaration,
414    )?;
415    writeln!(body, "}}").map_err(fmt_err)?;
416    emit_verbatim_at(
417        &mut body,
418        "",
419        &e.annotations,
420        PlacementKind::AfterDeclaration,
421    )?;
422    emit_verbatim_at(&mut body, "", &e.annotations, PlacementKind::EndFile)?;
423
424    let source = wrap_compilation_unit(pkg, &ImportSet::default(), &body);
425    Ok(JavaFile {
426        package_path: pkg.to_string(),
427        class_name: class,
428        source,
429    })
430}
431
432/// Union → ein sealed interface + ein Java-File pro Case-Record.
433/// Wir geben *einen* File mit sealed interface + nested case-records
434/// zurueck (Java erlaubt nested permits in einem File). Dadurch bleibt
435/// die Datei-Anzahl deterministisch.
436fn emit_union_files(
437    u: &UnionDef,
438    pkg: &str,
439    opts: &JavaGenOptions,
440) -> Result<Vec<JavaFile>, JavaGenError> {
441    let class = sanitize_identifier(&u.name.text)?;
442    let ind = indent_unit(opts);
443    let imports = ImportSet::default();
444
445    let _disc_ty = switch_type_to_java(&u.switch_type)?;
446
447    // Permit-Liste der Case-Records (per Member-Name eindeutig).
448    let mut permits: Vec<String> = Vec::new();
449    let mut case_records: Vec<(String, String, String)> = Vec::new(); // (record-name, field-ty, field-name)
450    for c in &u.cases {
451        let cpp_ty = type_for_declarator(&c.element.type_spec, &c.element.declarator)?;
452        let field_name = sanitize_identifier(&c.element.declarator.name().text)?;
453        // Record-Name: CapitalCase aus Field-Name.
454        let record_name = capitalize(&field_name);
455        if !permits.iter().any(|p| p == &record_name) {
456            permits.push(record_name.clone());
457            case_records.push((record_name, cpp_ty, field_name));
458        }
459    }
460    // Java verlangt fuer nested-records innerhalb des sealed
461    // interface qualifizierte Namen in der `permits`-Klausel —
462    // `permits A, B, C` schlaegt mit `cannot find symbol` fehl,
463    // `permits Foo.A, Foo.B, Foo.C` ist die korrekte Form.
464    //
465    // Findings-Anker: TS-3-Finding 5 (`docs/test-harness/plan.md`).
466    let permits_clause = if permits.is_empty() {
467        String::new()
468    } else {
469        let qualified: Vec<String> = permits.iter().map(|p| format!("{class}.{p}")).collect();
470        format!(" permits {}", qualified.join(", "))
471    };
472
473    let mut body = String::new();
474    emit_verbatim_at(&mut body, "", &u.annotations, PlacementKind::BeginFile)?;
475    emit_verbatim_at(
476        &mut body,
477        "",
478        &u.annotations,
479        PlacementKind::BeforeDeclaration,
480    )?;
481    writeln!(body, "public sealed interface {class}{permits_clause} {{").map_err(fmt_err)?;
482    emit_verbatim_at(
483        &mut body,
484        &ind,
485        &u.annotations,
486        PlacementKind::BeginDeclaration,
487    )?;
488
489    // Default-Marker fuer den default-Branch (Kommentar; Branch-Labels
490    // werden als Kommentar emittiert, nicht als Java-Konstrukt).
491    let mut has_default = false;
492    for c in &u.cases {
493        for label in &c.labels {
494            match label {
495                CaseLabel::Default => {
496                    has_default = true;
497                    writeln!(
498                        body,
499                        "{ind}// case default -> {}",
500                        c.element.declarator.name().text
501                    )
502                    .map_err(fmt_err)?;
503                }
504                CaseLabel::Value(expr) => {
505                    let val = const_expr_to_java(expr);
506                    writeln!(
507                        body,
508                        "{ind}// case {val} -> {}",
509                        c.element.declarator.name().text
510                    )
511                    .map_err(fmt_err)?;
512                }
513            }
514        }
515    }
516    if !has_default {
517        writeln!(body, "{ind}// no explicit 'default:' branch").map_err(fmt_err)?;
518    }
519    writeln!(body).map_err(fmt_err)?;
520
521    // Nested case-records.
522    for (record_name, field_ty, field_name) in &case_records {
523        writeln!(
524            body,
525            "{ind}record {record_name}({field_ty} {field_name}) implements {class} {{}}",
526        )
527        .map_err(fmt_err)?;
528    }
529    emit_verbatim_at(
530        &mut body,
531        &ind,
532        &u.annotations,
533        PlacementKind::EndDeclaration,
534    )?;
535    writeln!(body, "}}").map_err(fmt_err)?;
536    emit_verbatim_at(
537        &mut body,
538        "",
539        &u.annotations,
540        PlacementKind::AfterDeclaration,
541    )?;
542    emit_verbatim_at(&mut body, "", &u.annotations, PlacementKind::EndFile)?;
543
544    let source = wrap_compilation_unit(pkg, &imports, &body);
545    Ok(vec![JavaFile {
546        package_path: pkg.to_string(),
547        class_name: class,
548        source,
549    }])
550}
551
552fn emit_typedef_files(
553    t: &TypedefDecl,
554    pkg: &str,
555    _opts: &JavaGenOptions,
556) -> Result<Vec<JavaFile>, JavaGenError> {
557    // Java hat keine `using`/`typedef` — wir emittieren eine Wrapper-
558    // Klasse pro Alias (1 Wrapper-Field, named `value`).
559    let mut out = Vec::new();
560    for decl in &t.declarators {
561        let alias = sanitize_identifier(&decl.name().text)?;
562        let target = type_for_declarator(&t.type_spec, decl)?;
563        let imports = ImportSet::default();
564
565        let mut body = String::new();
566        writeln!(body, "public final class {alias} {{").map_err(fmt_err)?;
567        writeln!(body, "    private {target} value;").map_err(fmt_err)?;
568        writeln!(body).map_err(fmt_err)?;
569        writeln!(body, "    public {alias}() {{}}").map_err(fmt_err)?;
570        writeln!(
571            body,
572            "    public {alias}({target} value) {{ this.value = value; }}",
573        )
574        .map_err(fmt_err)?;
575        writeln!(body).map_err(fmt_err)?;
576        writeln!(body, "    public {target} value() {{ return value; }}").map_err(fmt_err)?;
577        writeln!(
578            body,
579            "    public void value({target} value) {{ this.value = value; }}",
580        )
581        .map_err(fmt_err)?;
582        writeln!(body, "}}").map_err(fmt_err)?;
583
584        let source = wrap_compilation_unit(pkg, &imports, &body);
585        out.push(JavaFile {
586            package_path: pkg.to_string(),
587            class_name: alias,
588            source,
589        });
590    }
591    Ok(out)
592}
593
594fn emit_exception_file(
595    e: &ExceptDecl,
596    pkg: &str,
597    opts: &JavaGenOptions,
598) -> Result<JavaFile, JavaGenError> {
599    let class = sanitize_identifier(&e.name.text)?;
600    let ind = indent_unit(opts);
601    let mut imports = ImportSet::default();
602    for m in &e.members {
603        collect_member_imports(m, &mut imports);
604    }
605
606    let mut body = String::new();
607    writeln!(body, "public class {class} extends RuntimeException {{").map_err(fmt_err)?;
608    for m in &e.members {
609        emit_member_field(&mut body, m, &ind)?;
610    }
611    writeln!(body).map_err(fmt_err)?;
612    writeln!(body, "{ind}public {class}() {{ super(); }}").map_err(fmt_err)?;
613    writeln!(
614        body,
615        "{ind}public {class}(String message) {{ super(message); }}",
616    )
617    .map_err(fmt_err)?;
618    writeln!(body).map_err(fmt_err)?;
619    for m in &e.members {
620        emit_member_accessors(&mut body, m, &ind)?;
621    }
622    writeln!(body, "}}").map_err(fmt_err)?;
623
624    let source = wrap_compilation_unit(pkg, &imports, &body);
625    Ok(JavaFile {
626        package_path: pkg.to_string(),
627        class_name: class,
628        source,
629    })
630}
631
632fn emit_const_holder(
633    c: &zerodds_idl::ast::ConstDecl,
634    pkg: &str,
635    _opts: &JavaGenOptions,
636) -> Result<JavaFile, JavaGenError> {
637    // IDL-`const` → public static final field in einer Holder-Class
638    // namens `<NAME>Constant`.
639    let name = sanitize_identifier(&c.name.text)?;
640    let class = format!("{name}Constant");
641    let java_ty = const_type_to_java(&c.type_)?;
642    let val = const_expr_to_java(&c.value);
643    let mut body = String::new();
644    writeln!(body, "public final class {class} {{").map_err(fmt_err)?;
645    writeln!(body, "    public static final {java_ty} {name} = {val};").map_err(fmt_err)?;
646    writeln!(body, "    private {class}() {{}}").map_err(fmt_err)?;
647    writeln!(body, "}}").map_err(fmt_err)?;
648    let source = wrap_compilation_unit(pkg, &ImportSet::default(), &body);
649    Ok(JavaFile {
650        package_path: pkg.to_string(),
651        class_name: class,
652        source,
653    })
654}
655
656// ---------------------------------------------------------------------------
657// Member-Helpers
658// ---------------------------------------------------------------------------
659
660fn emit_member_field(out: &mut String, m: &Member, ind: &str) -> Result<(), JavaGenError> {
661    let optional = has_optional_annotation(&m.annotations);
662    let ann_lines = member_annotation_lines(&m.annotations);
663    for decl in &m.declarators {
664        let java_ty = type_for_declarator(&m.type_spec, decl)?;
665        let name = sanitize_identifier(&decl.name().text)?;
666        let final_ty = if optional {
667            format!("java.util.Optional<{}>", boxed_for_optional(&m.type_spec))
668        } else {
669            java_ty
670        };
671        for ann in &ann_lines {
672            writeln!(out, "{ind}{ann}").map_err(fmt_err)?;
673        }
674        // Doc-Comment fuer unsigned-Workaround.
675        if let TypeSpec::Primitive(zerodds_idl::ast::PrimitiveType::Integer(i)) = &m.type_spec {
676            if is_unsigned(*i) {
677                writeln!(
678                    out,
679                    "{ind}/** unsigned IDL value (Java unsigned-workaround) */"
680                )
681                .map_err(fmt_err)?;
682            }
683        }
684        writeln!(out, "{ind}private {final_ty} {name};").map_err(fmt_err)?;
685    }
686    Ok(())
687}
688
689fn emit_member_accessors(out: &mut String, m: &Member, ind: &str) -> Result<(), JavaGenError> {
690    let optional = has_optional_annotation(&m.annotations);
691    for decl in &m.declarators {
692        let java_ty = type_for_declarator(&m.type_spec, decl)?;
693        let name = sanitize_identifier(&decl.name().text)?;
694        let cap = capitalize(&name);
695        let final_ty = if optional {
696            format!("java.util.Optional<{}>", boxed_for_optional(&m.type_spec))
697        } else {
698            java_ty.clone()
699        };
700        writeln!(
701            out,
702            "{ind}public {final_ty} get{cap}() {{ return {name}; }}"
703        )
704        .map_err(fmt_err)?;
705        writeln!(
706            out,
707            "{ind}public void set{cap}({final_ty} {name}) {{ this.{name} = {name}; }}",
708        )
709        .map_err(fmt_err)?;
710    }
711    Ok(())
712}
713
714fn boxed_for_optional(ts: &TypeSpec) -> String {
715    match ts {
716        TypeSpec::Primitive(p) => primitive_to_java_boxed(*p).to_string(),
717        TypeSpec::Scoped(s) => scoped_to_java(s),
718        TypeSpec::String(_) => "String".into(),
719        TypeSpec::Sequence(s) => {
720            // List<Boxed<T>>
721            let inner = match &*s.elem {
722                TypeSpec::Primitive(p) => primitive_to_java_boxed(*p).to_string(),
723                TypeSpec::Scoped(sn) => scoped_to_java(sn),
724                TypeSpec::String(_) => "String".into(),
725                _ => "Object".into(),
726            };
727            format!("java.util.List<{inner}>")
728        }
729        _ => "Object".into(),
730    }
731}
732
733// ---------------------------------------------------------------------------
734// TypeSpec / Declarator
735// ---------------------------------------------------------------------------
736
737/// Liefert den Java-Type-Ausdruck fuer Member (TypeSpec + Declarator).
738pub(crate) fn type_for_declarator(
739    ts: &TypeSpec,
740    decl: &Declarator,
741) -> Result<String, JavaGenError> {
742    let base = typespec_to_java(ts)?;
743    match decl {
744        Declarator::Simple(_) => Ok(base),
745        Declarator::Array(arr) => {
746            let mut suffix = String::new();
747            for _ in &arr.sizes {
748                suffix.push_str("[]");
749            }
750            Ok(format!("{base}{suffix}"))
751        }
752    }
753}
754
755/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
756pub(crate) fn typespec_to_java(ts: &TypeSpec) -> Result<String, JavaGenError> {
757    match ts {
758        TypeSpec::Primitive(p) => Ok(primitive_to_java(*p).to_string()),
759        TypeSpec::Scoped(s) => Ok(scoped_to_java(s)),
760        TypeSpec::Sequence(s) => {
761            let inner = match &*s.elem {
762                TypeSpec::Primitive(p) => primitive_to_java_boxed(*p).to_string(),
763                TypeSpec::Scoped(sn) => scoped_to_java(sn),
764                TypeSpec::String(_) => "String".into(),
765                other => typespec_to_java(other)?,
766            };
767            Ok(format!("java.util.List<{inner}>"))
768        }
769        TypeSpec::String(_) => Ok("String".into()),
770        TypeSpec::Map(m) => {
771            let k = match &*m.key {
772                TypeSpec::Primitive(p) => primitive_to_java_boxed(*p).to_string(),
773                TypeSpec::Scoped(sn) => scoped_to_java(sn),
774                TypeSpec::String(_) => "String".into(),
775                other => typespec_to_java(other)?,
776            };
777            let v = match &*m.value {
778                TypeSpec::Primitive(p) => primitive_to_java_boxed(*p).to_string(),
779                TypeSpec::Scoped(sn) => scoped_to_java(sn),
780                TypeSpec::String(_) => "String".into(),
781                other => typespec_to_java(other)?,
782            };
783            Ok(format!("java.util.Map<{k}, {v}>"))
784        }
785        TypeSpec::Fixed(_) => {
786            // Spec idl4-java §7.2.4.2.4: fixed<digits,scale> ->
787            // `java.math.BigDecimal` (Range-Check via
788            // `java.lang.ArithmeticException` zur Laufzeit, Scale via
789            // `setScale(scale)` im Codegen-Output).
790            Ok("java.math.BigDecimal".into())
791        }
792        TypeSpec::Any => {
793            // Spec idl4-java §7.3: any -> `org.omg.type.Any`. ZeroDDS-
794            // Mapping-Wahl: `java.lang.Object` (Reflection-basiert,
795            // Spec sagt explizit "implementation is middleware
796            // specific"). org.omg.type.Any-Wrapper-Variante moeglich,
797            // aber Object reicht fuer Java-Type-Repr-§8 Pfad.
798            Ok("Object".into())
799        }
800    }
801}
802
803pub(crate) fn switch_type_to_java(s: &SwitchTypeSpec) -> Result<String, JavaGenError> {
804    Ok(match s {
805        SwitchTypeSpec::Integer(i) => integer_to_java(*i).to_string(),
806        SwitchTypeSpec::Char => "char".into(),
807        SwitchTypeSpec::Boolean => "boolean".into(),
808        SwitchTypeSpec::Octet => "byte".into(),
809        SwitchTypeSpec::Scoped(s) => scoped_to_java(s),
810    })
811}
812
813fn const_type_to_java(t: &zerodds_idl::ast::ConstType) -> Result<String, JavaGenError> {
814    Ok(match t {
815        zerodds_idl::ast::ConstType::Integer(i) => integer_to_java(*i).to_string(),
816        zerodds_idl::ast::ConstType::Floating(f) => floating_to_java(*f).to_string(),
817        zerodds_idl::ast::ConstType::Boolean => "boolean".into(),
818        zerodds_idl::ast::ConstType::Char => "char".into(),
819        zerodds_idl::ast::ConstType::WideChar => "char".into(),
820        zerodds_idl::ast::ConstType::Octet => "byte".into(),
821        zerodds_idl::ast::ConstType::String { .. } => "String".into(),
822        zerodds_idl::ast::ConstType::Scoped(s) => scoped_to_java(s),
823        zerodds_idl::ast::ConstType::Fixed => "java.math.BigDecimal".into(),
824    })
825}
826
827fn scoped_to_java(s: &ScopedName) -> String {
828    let parts: Vec<String> = s.parts.iter().map(|p| p.text.clone()).collect();
829    parts.join(".")
830}
831
832// ---------------------------------------------------------------------------
833// Imports collection
834// ---------------------------------------------------------------------------
835
836#[derive(Debug, Default, Clone)]
837pub(crate) struct ImportSet {
838    #[allow(dead_code)]
839    imports: BTreeSet<&'static str>,
840}
841
842impl ImportSet {
843    #[allow(dead_code)]
844    fn add(&mut self, fqn: &'static str) {
845        self.imports.insert(fqn);
846    }
847}
848
849/// Hook fuer C5.4-b: hier kann die Import-Sammlung pro Member
850/// erweitert werden. C5.4-a nutzt durchgaengig FQN, daher No-op.
851#[allow(clippy::needless_pass_by_ref_mut)]
852fn collect_member_imports(_m: &Member, _inc: &mut ImportSet) {
853    // FQN-Strategie: java.util.List/Optional/Map werden inline als
854    // `java.util.<X>` referenziert, daher keine Import-Eintraege noetig.
855}
856
857// ---------------------------------------------------------------------------
858// Compilation-Unit-Wrapping
859// ---------------------------------------------------------------------------
860
861pub(crate) fn wrap_compilation_unit(pkg: &str, _imports: &ImportSet, body: &str) -> String {
862    let mut out = String::new();
863    let _ = writeln!(out, "// Generated by zerodds idl-java. Do not edit.");
864    if !pkg.is_empty() {
865        let _ = writeln!(out, "package {pkg};");
866        let _ = writeln!(out);
867    }
868    // Imports werden derzeit per FQN ersetzt — keine import-Statements
869    // erforderlich. Das haelt den Diff stabil und vermeidet Konflikte
870    // bei Type-Namen wie `List`/`Map`, falls die IDL einen Type so
871    // benannt hat.
872    out.push_str(body);
873    out
874}
875
876// ---------------------------------------------------------------------------
877// ConstExpr → Java-Literal
878// ---------------------------------------------------------------------------
879
880/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
881pub(crate) fn const_expr_to_java(e: &ConstExpr) -> String {
882    match e {
883        ConstExpr::Literal(l) => literal_to_java(l),
884        ConstExpr::Scoped(s) => scoped_to_java(s),
885        ConstExpr::Unary { op, operand, .. } => {
886            let prefix = match op {
887                zerodds_idl::ast::UnaryOp::Plus => "+",
888                zerodds_idl::ast::UnaryOp::Minus => "-",
889                zerodds_idl::ast::UnaryOp::BitNot => "~",
890            };
891            format!("{prefix}{}", const_expr_to_java(operand))
892        }
893        ConstExpr::Binary { op, lhs, rhs, .. } => {
894            let opstr = match op {
895                zerodds_idl::ast::BinaryOp::Or => "|",
896                zerodds_idl::ast::BinaryOp::Xor => "^",
897                zerodds_idl::ast::BinaryOp::And => "&",
898                zerodds_idl::ast::BinaryOp::Shl => "<<",
899                zerodds_idl::ast::BinaryOp::Shr => ">>",
900                zerodds_idl::ast::BinaryOp::Add => "+",
901                zerodds_idl::ast::BinaryOp::Sub => "-",
902                zerodds_idl::ast::BinaryOp::Mul => "*",
903                zerodds_idl::ast::BinaryOp::Div => "/",
904                zerodds_idl::ast::BinaryOp::Mod => "%",
905            };
906            format!(
907                "({} {opstr} {})",
908                const_expr_to_java(lhs),
909                const_expr_to_java(rhs)
910            )
911        }
912    }
913}
914
915fn literal_to_java(l: &Literal) -> String {
916    match l.kind {
917        LiteralKind::Boolean
918        | LiteralKind::Integer
919        | LiteralKind::Floating
920        | LiteralKind::Char
921        | LiteralKind::WideChar
922        | LiteralKind::String
923        | LiteralKind::WideString
924        | LiteralKind::Fixed => l.raw.clone(),
925    }
926}
927
928// ---------------------------------------------------------------------------
929// Annotation-Helpers
930// ---------------------------------------------------------------------------
931
932fn has_optional_annotation(anns: &[Annotation]) -> bool {
933    has_named_annotation(anns, "optional")
934}
935
936fn has_named_annotation(anns: &[Annotation], name: &str) -> bool {
937    anns.iter().any(|a| {
938        a.name.parts.last().is_some_and(|p| p.text == name)
939            && matches!(a.params, AnnotationParams::None | AnnotationParams::Empty)
940    })
941}
942
943// ---------------------------------------------------------------------------
944// Inheritance-Cycle-Detection
945// ---------------------------------------------------------------------------
946
947/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
948fn collect_inheritance_edges(
949    defs: &[Definition],
950    parents: &mut HashMap<String, String>,
951    prefix: &str,
952) {
953    for d in defs {
954        match d {
955            Definition::Module(m) => {
956                let new_prefix = if prefix.is_empty() {
957                    m.name.text.clone()
958                } else {
959                    format!("{prefix}.{}", m.name.text)
960                };
961                collect_inheritance_edges(&m.definitions, parents, &new_prefix);
962            }
963            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
964                let key = if prefix.is_empty() {
965                    s.name.text.clone()
966                } else {
967                    format!("{prefix}.{}", s.name.text)
968                };
969                if let Some(b) = &s.base {
970                    let base_str = b
971                        .parts
972                        .iter()
973                        .map(|p| p.text.clone())
974                        .collect::<Vec<_>>()
975                        .join(".");
976                    parents.insert(key, base_str);
977                }
978            }
979            _ => {}
980        }
981    }
982}
983
984fn detect_inheritance_cycles(spec: &Specification) -> Result<(), JavaGenError> {
985    let mut parents: HashMap<String, String> = HashMap::new();
986    collect_inheritance_edges(&spec.definitions, &mut parents, "");
987
988    for start in parents.keys() {
989        let mut current = start.clone();
990        let mut visited: BTreeSet<String> = BTreeSet::new();
991        visited.insert(current.clone());
992        while let Some(p) = parents.get(&current) {
993            let resolved = parents
994                .keys()
995                .find(|k| *k == p || k.ends_with(&format!(".{p}")))
996                .cloned()
997                .unwrap_or_else(|| p.clone());
998            if visited.contains(&resolved) {
999                return Err(JavaGenError::InheritanceCycle {
1000                    type_name: short_name(&resolved),
1001                });
1002            }
1003            visited.insert(resolved.clone());
1004            if resolved == current {
1005                return Err(JavaGenError::InheritanceCycle {
1006                    type_name: short_name(&resolved),
1007                });
1008            }
1009            current = resolved;
1010            if !parents.contains_key(&current) {
1011                break;
1012            }
1013        }
1014    }
1015    Ok(())
1016}
1017
1018fn short_name(s: &str) -> String {
1019    s.rsplit('.').next().unwrap_or(s).to_string()
1020}
1021
1022// ---------------------------------------------------------------------------
1023// Helpers
1024// ---------------------------------------------------------------------------
1025
1026pub(crate) fn indent_unit(opts: &JavaGenOptions) -> String {
1027    " ".repeat(opts.indent_width)
1028}
1029
1030pub(crate) fn capitalize(s: &str) -> String {
1031    let mut chars = s.chars();
1032    match chars.next() {
1033        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
1034        None => String::new(),
1035    }
1036}
1037
1038pub(crate) fn fmt_err(_: core::fmt::Error) -> JavaGenError {
1039    JavaGenError::Internal("string formatting failed".into())
1040}
1041
1042/// Wrap-Helper fuer Bitset/Bitmask: wickelt Header + Body in eine
1043/// Compilation-Unit. Identisch zu [`wrap_compilation_unit`] nur ohne
1044/// Import-Argument.
1045pub(crate) fn wrap_compilation_unit_default(pkg: &str, body: &str) -> String {
1046    wrap_compilation_unit(pkg, &ImportSet::default(), body)
1047}
1048
1049// ---------------------------------------------------------------------------
1050// Multi-Inheritance — Interface-Pattern (C5.4-b §3)
1051// ---------------------------------------------------------------------------
1052
1053/// Sammelt fuer jede Struct-Definition den (kurzen) Namen ihres
1054/// direkten Vorgaengers. IDL4-`struct`-Inheritance ist single — der
1055/// Codegen erzeugt aus der transitiven Kette die `extends + implements
1056/// XInterface, YInterface`-Form.
1057fn collect_base_chain_index(spec: &Specification) -> std::collections::HashMap<String, String> {
1058    let mut out: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1059    /// zerodds-lint: recursion-depth 32
1060    ///
1061    /// Modul-Hierarchie in IDL-Files: typische Tiefe 2-4
1062    /// (`org::omg::dds::core`), 32 deckt Edge-Cases.
1063    fn visit(defs: &[Definition], out: &mut std::collections::HashMap<String, String>) {
1064        for d in defs {
1065            match d {
1066                Definition::Module(m) => visit(&m.definitions, out),
1067                Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
1068                    if let Some(b) = &s.base {
1069                        out.insert(s.name.text.clone(), scoped_to_short(b));
1070                    }
1071                }
1072                _ => {}
1073            }
1074        }
1075    }
1076    visit(&spec.definitions, &mut out);
1077    out
1078}
1079
1080/// Liefert die transitiven Vorfahren-Namen *jenseits* der direkten
1081/// Basis (alle Grandparents). Bei `A : B`, `B : C`, `C : D` liefert
1082/// `transitive_ancestors_beyond_base("A", ctx) → ["C", "D"]`.
1083fn transitive_ancestors_beyond_base(name: &str, ctx: &EmitCtx) -> Vec<String> {
1084    let mut out: Vec<String> = Vec::new();
1085    let direct = match ctx.parent_of.get(name) {
1086        Some(p) => p.clone(),
1087        None => return out,
1088    };
1089    let mut current = direct;
1090    let mut guard = 0usize;
1091    while let Some(p) = ctx.parent_of.get(&current) {
1092        if guard > 64 {
1093            break;
1094        }
1095        guard += 1;
1096        out.push(p.clone());
1097        current = p.clone();
1098    }
1099    out
1100}
1101
1102fn scoped_to_short(s: &ScopedName) -> String {
1103    s.parts.last().map(|p| p.text.clone()).unwrap_or_default()
1104}
1105
1106/// Emittiert ein Companion-Interface `<Name>Interface.java` mit
1107/// Default-Methoden, die das Bean-Pattern-Read-Only-Abbild der
1108/// Members spiegeln. Dadurch koennen Sub-Sub-Klassen den Vorfahren
1109/// ueber `implements <Name>Interface` einbinden, ohne dass die
1110/// JVM-Class-File-Constraints (single `extends`) verletzt werden.
1111fn emit_struct_companion_interface(
1112    s: &StructDef,
1113    pkg: &str,
1114    opts: &JavaGenOptions,
1115) -> Result<JavaFile, JavaGenError> {
1116    let class = sanitize_identifier(&s.name.text)?;
1117    let interface_name = format!("{class}Interface");
1118    let ind = indent_unit(opts);
1119    let mut body = String::new();
1120    writeln!(
1121        body,
1122        "/** Companion interface for {class}; lets sub-sub-classes \
1123         participate in the {class} contract via `implements`. */",
1124    )
1125    .map_err(fmt_err)?;
1126    writeln!(body, "public interface {interface_name} {{").map_err(fmt_err)?;
1127    // Default-Methoden — wir geben die Getter-Signaturen mit
1128    // `default`-Implementierung wieder, die per cast auf die
1129    // konkrete Class delegiert. Da wir die Klasse zur Compile-Zeit
1130    // kennen (jede `implements`-Stelle ist eine Subklasse von
1131    // `class`), erzeugen wir hier nur abstrakte Methoden — die
1132    // konkrete Class liefert die Implementierung als ueblicher
1133    // Bean-Getter.
1134    for m in &s.members {
1135        let opt = has_optional_annotation(&m.annotations);
1136        for decl in &m.declarators {
1137            let java_ty = type_for_declarator(&m.type_spec, decl)?;
1138            let name = sanitize_identifier(&decl.name().text)?;
1139            let cap = capitalize(&name);
1140            let final_ty = if opt {
1141                format!("java.util.Optional<{}>", boxed_for_optional(&m.type_spec))
1142            } else {
1143                java_ty
1144            };
1145            writeln!(body, "{ind}{final_ty} get{cap}();").map_err(fmt_err)?;
1146        }
1147    }
1148    writeln!(body, "}}").map_err(fmt_err)?;
1149    let source = wrap_compilation_unit_default(pkg, &body);
1150    Ok(JavaFile {
1151        package_path: pkg.to_string(),
1152        class_name: interface_name,
1153        source,
1154    })
1155}
1156
1157// Marker, damit unused-imports keine Warnings produziert (z.B.
1158// `IntegerType`/`integer_to_java_boxed`, falls Detail-Use spaeter
1159// wegfaellt).
1160#[allow(dead_code)]
1161fn _unused_marker(_i: IntegerType) {
1162    let _ = integer_to_java_boxed;
1163    let _ = floating_to_java_boxed;
1164}
1165
1166// ---------------------------------------------------------------------------
1167// RPC-Service-Bridge (DDS-RPC §7.11.2)
1168// ---------------------------------------------------------------------------
1169
1170/// Spec idl4-java §7.6: valuetype -> 2 Java-Klassen
1171/// (`<Name>Abstract` abstract + `<Name>` non-abstract).
1172/// public state -> public abstract Bean-Accessoren; private state ->
1173/// protected abstract Accessoren; factory -> void-Method.
1174fn emit_value_type_files(
1175    v: &zerodds_idl::ast::ValueDef,
1176    pkg: &str,
1177    opts: &JavaGenOptions,
1178) -> Result<Vec<JavaFile>, JavaGenError> {
1179    use zerodds_idl::ast::{Export, StateVisibility, ValueElement};
1180
1181    let class = sanitize_identifier(&v.name.text)?;
1182    let abstract_name = format!("{class}Abstract");
1183    let ind = indent_unit(opts);
1184    let imports = ImportSet::default();
1185
1186    // Abstract base class.
1187    let mut body = String::new();
1188    let extends = match &v.inheritance {
1189        Some(inh) if !inh.bases.is_empty() => {
1190            // Java erlaubt nur eine super-Class — wir nehmen die erste base.
1191            let base = scoped_to_java(&inh.bases[0]);
1192            format!(" extends {base}Abstract")
1193        }
1194        _ => String::new(),
1195    };
1196    let supports = match &v.inheritance {
1197        Some(inh) if !inh.supports.is_empty() => {
1198            let s: Vec<String> = inh.supports.iter().map(scoped_to_java).collect();
1199            format!(" implements {}", s.join(", "))
1200        }
1201        _ => String::new(),
1202    };
1203
1204    writeln!(
1205        body,
1206        "public abstract class {abstract_name}{extends}{supports} {{"
1207    )
1208    .map_err(fmt_err)?;
1209
1210    for el in &v.elements {
1211        match el {
1212            ValueElement::State(s) => {
1213                let ty = typespec_to_java(&s.type_spec)?;
1214                let visibility = match s.visibility {
1215                    StateVisibility::Public => "public",
1216                    StateVisibility::Private => "protected",
1217                };
1218                for d in &s.declarators {
1219                    let n = sanitize_identifier(&d.name().text)?;
1220                    writeln!(body, "{ind}{visibility} abstract {ty} get_{n}();")
1221                        .map_err(fmt_err)?;
1222                    writeln!(body, "{ind}{visibility} abstract void set_{n}({ty} value);")
1223                        .map_err(fmt_err)?;
1224                }
1225            }
1226            ValueElement::Init(i) => {
1227                let params: Vec<String> = i
1228                    .params
1229                    .iter()
1230                    .map(|p| -> Result<String, JavaGenError> {
1231                        let ty = typespec_to_java(&p.type_spec)?;
1232                        let pname = sanitize_identifier(&p.name.text)?;
1233                        Ok(format!("{ty} {pname}"))
1234                    })
1235                    .collect::<Result<_, _>>()?;
1236                writeln!(
1237                    body,
1238                    "{ind}public abstract void {}({});",
1239                    sanitize_identifier(&i.name.text)?,
1240                    params.join(", ")
1241                )
1242                .map_err(fmt_err)?;
1243            }
1244            ValueElement::Export(Export::Op(op)) => {
1245                let ret = match &op.return_type {
1246                    None => "void".to_string(),
1247                    Some(t) => typespec_to_java(t)?,
1248                };
1249                let params: Vec<String> = op
1250                    .params
1251                    .iter()
1252                    .map(|p| -> Result<String, JavaGenError> {
1253                        let ty = typespec_to_java(&p.type_spec)?;
1254                        let pname = sanitize_identifier(&p.name.text)?;
1255                        Ok(format!("{ty} {pname}"))
1256                    })
1257                    .collect::<Result<_, _>>()?;
1258                writeln!(
1259                    body,
1260                    "{ind}public abstract {ret} {}({});",
1261                    sanitize_identifier(&op.name.text)?,
1262                    params.join(", ")
1263                )
1264                .map_err(fmt_err)?;
1265            }
1266            _ => {}
1267        }
1268    }
1269    writeln!(body, "}}").map_err(fmt_err)?;
1270
1271    let abstract_source = wrap_compilation_unit(pkg, &imports, &body);
1272    let abstract_file = JavaFile {
1273        package_path: pkg.to_string(),
1274        class_name: abstract_name.clone(),
1275        source: abstract_source,
1276    };
1277
1278    // Concrete subclass-Skelett.
1279    let concrete_body = format!(
1280        "public class {class} extends {abstract_name} {{\n{ind}// User-Implementation hier\n}}\n"
1281    );
1282    let concrete_source = wrap_compilation_unit(pkg, &imports, &concrete_body);
1283    let concrete_file = JavaFile {
1284        package_path: pkg.to_string(),
1285        class_name: class,
1286        source: concrete_source,
1287    };
1288
1289    Ok(vec![abstract_file, concrete_file])
1290}
1291
1292/// Spec idl4-java §7.4: IDL interface -> Java public interface mit
1293/// Method pro Operation (raises -> throws), Property pro Attribute.
1294fn emit_non_service_interface_file(
1295    iface: &InterfaceDef,
1296    pkg: &str,
1297    opts: &JavaGenOptions,
1298) -> Result<JavaFile, JavaGenError> {
1299    use zerodds_idl::ast::Export;
1300
1301    let class = sanitize_identifier(&iface.name.text)?;
1302    let imports = ImportSet::default();
1303    let ind = indent_unit(opts);
1304    let mut body = String::new();
1305
1306    let extends = if iface.bases.is_empty() {
1307        String::new()
1308    } else {
1309        let bases: Vec<String> = iface.bases.iter().map(scoped_to_java).collect();
1310        format!(" extends {}", bases.join(", "))
1311    };
1312    writeln!(body, "public interface {class}{extends} {{").map_err(fmt_err)?;
1313
1314    for export in &iface.exports {
1315        match export {
1316            Export::Op(op) => {
1317                let ret = match &op.return_type {
1318                    None => "void".to_string(),
1319                    Some(t) => typespec_to_java(t)?,
1320                };
1321                let params: Vec<String> = op
1322                    .params
1323                    .iter()
1324                    .map(|p| -> Result<String, JavaGenError> {
1325                        let ty = typespec_to_java(&p.type_spec)?;
1326                        let pname = sanitize_identifier(&p.name.text)?;
1327                        Ok(format!("{ty} {pname}"))
1328                    })
1329                    .collect::<Result<_, _>>()?;
1330                let throws = if op.raises.is_empty() {
1331                    String::new()
1332                } else {
1333                    let raises: Vec<String> = op.raises.iter().map(scoped_to_java).collect();
1334                    format!(" throws {}", raises.join(", "))
1335                };
1336                writeln!(
1337                    body,
1338                    "{ind}{ret} {}({}){throws};",
1339                    sanitize_identifier(&op.name.text)?,
1340                    params.join(", ")
1341                )
1342                .map_err(fmt_err)?;
1343            }
1344            Export::Attr(attr) => {
1345                let ty = typespec_to_java(&attr.type_spec)?;
1346                let aname = sanitize_identifier(&attr.name.text)?;
1347                writeln!(body, "{ind}{ty} get_{aname}();").map_err(fmt_err)?;
1348                if !attr.readonly {
1349                    writeln!(body, "{ind}void set_{aname}({ty} value);").map_err(fmt_err)?;
1350                }
1351            }
1352            _ => {
1353                // Embedded type/const/exception: aktuell nicht implementiert.
1354            }
1355        }
1356    }
1357    writeln!(body, "}}").map_err(fmt_err)?;
1358
1359    let source = wrap_compilation_unit(pkg, &imports, &body);
1360    Ok(JavaFile {
1361        package_path: pkg.to_string(),
1362        class_name: class,
1363        source,
1364    })
1365}
1366
1367/// `true` wenn die Interface-Annotations `@service` enthalten — dann
1368/// behandeln wir das Interface als RPC-Service und delegieren an
1369/// [`crate::rpc`].
1370fn is_service_interface(iface: &InterfaceDef) -> bool {
1371    iface
1372        .annotations
1373        .iter()
1374        .any(|a| a.name.parts.last().is_some_and(|p| p.text == "service"))
1375}
1376
1377/// Emittiert die fuenf RPC-Files fuer ein `@service`-Interface plus die
1378/// `exception`-Files, die in den `raises`-Klauseln referenziert werden
1379/// (lokal im Interface-Body deklariert).
1380fn emit_service_interface_files(
1381    iface: &InterfaceDef,
1382    pkg: &str,
1383    opts: &JavaGenOptions,
1384    files: &mut Vec<JavaFile>,
1385) -> Result<(), JavaGenError> {
1386    use zerodds_idl::ast::Export;
1387    use zerodds_rpc::annotations::lower_rpc_annotations;
1388    use zerodds_rpc::service_mapping::lower_service;
1389
1390    // Inner exceptions — emit als RuntimeException-Subclasses, ueber
1391    // den existierenden Exception-Pfad.
1392    for export in &iface.exports {
1393        if let Export::Except(e) = export {
1394            files.push(emit_exception_file(e, pkg, opts)?);
1395        }
1396    }
1397
1398    // Lower IDL → ServiceDef.
1399    let lowered = lower_rpc_annotations(&iface.annotations);
1400    let svc = lower_service(iface, &lowered).map_err(|e| JavaGenError::Internal(e.to_string()))?;
1401
1402    // Emit die fuenf Java-Files.
1403    let svc_files = crate::rpc::emit_service_files(&svc, pkg, opts)?;
1404    files.extend(svc_files);
1405
1406    Ok(())
1407}