Skip to main content

alef_backend_java/gen_bindings/
mod.rs

1use ahash::AHashSet;
2use alef_codegen::naming::to_class_name;
3use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
4use alef_core::config::{BridgeBinding, JavaBuilderMode, Language, ResolvedCrateConfig};
5use alef_core::ir::{ApiSurface, TypeRef};
6use std::collections::HashSet;
7use std::path::PathBuf;
8
9mod facade;
10mod ffi_class;
11mod helpers;
12mod line_wrap;
13mod marshal;
14mod native_lib;
15mod trait_bridge;
16mod types;
17
18use facade::gen_facade_class;
19use ffi_class::gen_main_class;
20use helpers::{gen_exception_class, gen_infrastructure_exception_class};
21use native_lib::gen_native_lib;
22use types::{gen_byte_array_serializer, gen_enum_class, gen_opaque_handle_class, gen_record_type};
23
24pub struct JavaBackend;
25
26impl JavaBackend {
27    /// Convert crate name to main class name (PascalCase + "Rs" suffix).
28    ///
29    /// The "Rs" suffix ensures the raw FFI wrapper class has a distinct name from
30    /// the public facade class (which strips the "Rs" suffix). Without this, the
31    /// facade would delegate to itself, causing infinite recursion.
32    fn resolve_main_class(api: &ApiSurface) -> String {
33        let base = to_class_name(&api.crate_name.replace('-', "_"));
34        if base.ends_with("Rs") {
35            base
36        } else {
37            format!("{}Rs", base)
38        }
39    }
40}
41
42fn effective_exclude_types(config: &ResolvedCrateConfig) -> HashSet<String> {
43    let mut exclude_types: HashSet<String> = config
44        .ffi
45        .as_ref()
46        .map(|ffi| ffi.exclude_types.iter().cloned().collect())
47        .unwrap_or_default();
48    if let Some(java) = &config.java {
49        exclude_types.extend(java.exclude_types.iter().cloned());
50    }
51    exclude_types
52}
53
54fn references_excluded_type(ty: &TypeRef, exclude_types: &HashSet<String>) -> bool {
55    exclude_types.iter().any(|name| ty.references_named(name))
56}
57
58fn signature_references_excluded_type(
59    params: &[alef_core::ir::ParamDef],
60    return_type: &TypeRef,
61    exclude_types: &HashSet<String>,
62) -> bool {
63    references_excluded_type(return_type, exclude_types)
64        || params
65            .iter()
66            .any(|param| references_excluded_type(&param.ty, exclude_types))
67}
68
69fn api_without_excluded_types(api: &ApiSurface, exclude_types: &HashSet<String>) -> ApiSurface {
70    let mut filtered = api.clone();
71    filtered.types.retain(|typ| !exclude_types.contains(&typ.name));
72    for typ in &mut filtered.types {
73        typ.fields
74            .retain(|field| !references_excluded_type(&field.ty, exclude_types));
75        typ.methods
76            .retain(|method| !signature_references_excluded_type(&method.params, &method.return_type, exclude_types));
77    }
78    filtered
79        .enums
80        .retain(|enum_def| !exclude_types.contains(&enum_def.name));
81    for enum_def in &mut filtered.enums {
82        for variant in &mut enum_def.variants {
83            variant
84                .fields
85                .retain(|field| !references_excluded_type(&field.ty, exclude_types));
86        }
87    }
88    filtered
89        .functions
90        .retain(|func| !signature_references_excluded_type(&func.params, &func.return_type, exclude_types));
91    filtered.errors.retain(|error| !exclude_types.contains(&error.name));
92    filtered
93}
94
95impl Backend for JavaBackend {
96    fn name(&self) -> &str {
97        "java"
98    }
99
100    fn language(&self) -> Language {
101        Language::Java
102    }
103
104    fn capabilities(&self) -> Capabilities {
105        Capabilities {
106            supports_async: true,
107            supports_classes: true,
108            supports_enums: true,
109            supports_option: true,
110            supports_result: true,
111            ..Capabilities::default()
112        }
113    }
114
115    fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
116        let exclude_types = effective_exclude_types(config);
117        let filtered_api;
118        let api = if exclude_types.is_empty() {
119            api
120        } else {
121            filtered_api = api_without_excluded_types(api, &exclude_types);
122            &filtered_api
123        };
124        let package = config.java_package();
125        let prefix = config.ffi_prefix();
126        let main_class = Self::resolve_main_class(api);
127        let package_path = package.replace('.', "/");
128
129        let output_dir = config
130            .output_for("java")
131            .map(|p| p.to_string_lossy().into_owned())
132            .unwrap_or_else(|| "packages/java/src/main/java/".to_string());
133
134        // If output_dir already ends with the package path (user configured the full path),
135        // use it as-is. Otherwise, append the package path.
136        let base_path = if output_dir.ends_with(&package_path) || output_dir.ends_with(&format!("{}/", package_path)) {
137            PathBuf::from(&output_dir)
138        } else {
139            PathBuf::from(&output_dir).join(&package_path)
140        };
141
142        // Collect bridge param names and type aliases so we can strip them from generated
143        // function signatures and emit convertWithVisitor instead.
144        let bridge_param_names: HashSet<String> = config
145            .trait_bridges
146            .iter()
147            .filter_map(|b| b.param_name.clone())
148            .collect();
149        let bridge_type_aliases: HashSet<String> = config
150            .trait_bridges
151            .iter()
152            .filter_map(|b| b.type_alias.clone())
153            .collect();
154        // Generate visitor support when visitor_callbacks is enabled in FFI config (canonical check),
155        // OR when any trait bridge is bound via options_field (Java-specific activation path).
156        let has_visitor_pattern = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false)
157            || config
158                .trait_bridges
159                .iter()
160                .any(|b| b.bind_via == BridgeBinding::OptionsField);
161        let bridge_associated_types = config.bridge_associated_types();
162
163        let mut files = Vec::new();
164
165        // 0. package-info.java - required by Checkstyle
166        let description = config
167            .scaffold
168            .as_ref()
169            .and_then(|s| s.description.as_deref())
170            .unwrap_or("Generated Java bindings.");
171        files.push(GeneratedFile {
172            path: base_path.join("package-info.java"),
173            content: format!(
174                "/**\n * {description}\n */\npackage {package};\n",
175                description = description,
176                package = package,
177            ),
178            generated_header: true,
179        });
180
181        // 1. NativeLib.java - FFI method handles
182        files.push(GeneratedFile {
183            path: base_path.join("NativeLib.java"),
184            content: gen_native_lib(api, config, &package, &prefix, has_visitor_pattern),
185            generated_header: true,
186        });
187
188        // 2. Main wrapper class
189        files.push(GeneratedFile {
190            path: base_path.join(format!("{}.java", main_class)),
191            content: gen_main_class(
192                api,
193                config,
194                &package,
195                &main_class,
196                &prefix,
197                &bridge_param_names,
198                &bridge_type_aliases,
199                has_visitor_pattern,
200            ),
201            generated_header: true,
202        });
203
204        // 3. Exception class
205        files.push(GeneratedFile {
206            path: base_path.join(format!("{}Exception.java", main_class)),
207            content: gen_exception_class(&package, &main_class),
208            generated_header: true,
209        });
210
211        // 3b. Infrastructure exception classes for FFI error codes 1 and 2.
212        // These are always emitted because checkLastError() hardcodes:
213        //   case 1 -> throw new InvalidInputException(msg);
214        //   case 2 -> throw new ConversionErrorException(msg);
215        // Code 1 = null pointer / invalid UTF-8 in an input arg (invalid input).
216        // Code 2 = JSON serialisation/deserialisation failure (type conversion).
217        for (class_name, code, doc) in [
218            (
219                "InvalidInputException",
220                1i32,
221                "Exception thrown when input validation fails.",
222            ),
223            (
224                "ConversionErrorException",
225                2i32,
226                "Exception thrown when type conversion fails.",
227            ),
228        ] {
229            files.push(GeneratedFile {
230                path: base_path.join(format!("{}.java", class_name)),
231                content: gen_infrastructure_exception_class(&package, &main_class, class_name, code, doc),
232                generated_header: true,
233            });
234        }
235
236        // Untagged unions with data variants now emit as JsonNode-wrapper classes
237        // (see gen_java_untagged_wrapper). The set is intentionally empty so that
238        // record fields keep their wrapper type instead of being downcast to Object.
239        let complex_enums: AHashSet<String> = AHashSet::new();
240
241        // Collect sealed union types with unwrapped/tuple variants that need custom deserializers.
242        // When a record field references one of these types, we need to add a @JsonDeserialize
243        // annotation to the field so Jackson uses the custom deserializer.
244        let sealed_unions_with_unwrapped: AHashSet<String> = api
245            .enums
246            .iter()
247            .filter(|e| {
248                e.serde_tag.is_some()
249                    && e.variants
250                        .iter()
251                        .any(|v| v.fields.len() == 1 && helpers::is_tuple_field_name(&v.fields[0].name))
252            })
253            .map(|e| e.name.clone())
254            .collect();
255
256        // Resolve language-level serde rename strategy (always wins over IR type-level).
257        let lang_rename_all = config.serde_rename_all_for_language(Language::Java);
258
259        // 4. Record types
260        // Include non-opaque types that either have fields OR are serializable unit structs
261        // (has_serde + has_default, empty fields). Unit structs like `ExcelMetadata` need a
262        // concrete Java class so they can be referenced as record components in tagged-union
263        // variant records (e.g. FormatMetadata.Excel(@JsonUnwrapped ExcelMetadata value)).
264        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
265            let is_unit_serde = !typ.is_opaque && typ.fields.is_empty() && typ.has_serde;
266            if !typ.is_opaque && (!typ.fields.is_empty() || is_unit_serde) {
267                // Skip types that gen_visitor handles with richer visitor-specific versions
268                if has_visitor_pattern && bridge_associated_types.contains(typ.name.as_str()) {
269                    continue;
270                }
271                let builder_mode = config
272                    .java
273                    .as_ref()
274                    .map(|j| j.dto.builder)
275                    .unwrap_or(JavaBuilderMode::Auto);
276                files.push(GeneratedFile {
277                    path: base_path.join(format!("{}.java", typ.name)),
278                    content: gen_record_type(
279                        &package,
280                        typ,
281                        &complex_enums,
282                        &sealed_unions_with_unwrapped,
283                        &lang_rename_all,
284                        has_visitor_pattern,
285                        &main_class,
286                        builder_mode,
287                    ),
288                    generated_header: true,
289                });
290                // The builder is now emitted as a nested static class inside the record file —
291                // no separate *Builder.java file is created.
292            }
293        }
294
295        // 4a. Utility serializer for byte[] → JSON int-array (needed when any record
296        // has a non-optional Bytes field). Jackson's default byte[] serialiser emits
297        // base64, which Rust's serde Vec<u8> cannot accept. Emit the class once.
298        let needs_bytes_serializer = api
299            .types
300            .iter()
301            .any(|t| !t.is_opaque && t.fields.iter().any(|f| !f.optional && matches!(f.ty, TypeRef::Bytes)));
302        if needs_bytes_serializer {
303            files.push(GeneratedFile {
304                path: base_path.join("ByteArrayToIntArraySerializer.java"),
305                content: gen_byte_array_serializer(&package),
306                generated_header: true,
307            });
308        }
309
310        // 4b. Opaque handle types
311        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
312            if typ.is_opaque {
313                files.push(GeneratedFile {
314                    path: base_path.join(format!("{}.java", typ.name)),
315                    content: gen_opaque_handle_class(&package, typ, &prefix, &config.adapters, &main_class),
316                    generated_header: true,
317                });
318            }
319        }
320
321        // 5. Enums
322        for enum_def in &api.enums {
323            // Skip enums that gen_visitor handles with richer visitor-specific versions
324            if has_visitor_pattern && bridge_associated_types.contains(enum_def.name.as_str()) {
325                continue;
326            }
327            files.push(GeneratedFile {
328                path: base_path.join(format!("{}.java", enum_def.name)),
329                content: gen_enum_class(&package, enum_def, &main_class),
330                generated_header: true,
331            });
332        }
333
334        // 6. Error exception classes
335        //
336        // Filter out variants whose generated class name collides with the FFI infrastructure
337        // exceptions emitted at step 3b. Both paths target the same .java file; without this
338        // filter, the gen_java_error_types content was overwriting (or worse, mangling — the
339        // InvalidInputException file ended up with a duplicate constructor block appended
340        // after the closing brace) the canonical infrastructure-emitted class.
341        let infrastructure_exception_names: AHashSet<&str> = ["InvalidInputException", "ConversionErrorException"]
342            .into_iter()
343            .collect();
344        let mut emitted_exception_names: AHashSet<String> = AHashSet::new();
345        for error in &api.errors {
346            for (class_name, content) in alef_codegen::error_gen::gen_java_error_types(error, &package) {
347                if infrastructure_exception_names.contains(class_name.as_str()) {
348                    continue;
349                }
350                if !emitted_exception_names.insert(class_name.clone()) {
351                    continue;
352                }
353                files.push(GeneratedFile {
354                    path: base_path.join(format!("{}.java", class_name)),
355                    content,
356                    generated_header: true,
357                });
358            }
359        }
360
361        // 7. Visitor support files (only when ConversionOptions/ConversionResult types exist)
362        if has_visitor_pattern {
363            for (filename, content) in crate::gen_visitor::gen_visitor_files(&package, &main_class) {
364                files.push(GeneratedFile {
365                    path: base_path.join(filename),
366                    content,
367                    generated_header: false, // already has header comment
368                });
369            }
370        }
371
372        // 8. Trait bridge plugin registration files
373        // Emits two files per trait: I{Trait}.java (managed interface) and
374        // {Trait}Bridge.java (Panama upcall stubs + register/unregister helpers).
375        //
376        // Set of struct + enum names that get a generated companion Java class.
377        // Trait method signatures referencing types outside this set (e.g. excluded
378        // internal types like `InternalDocument`) are JSON-bridged as Strings.
379        let visible_type_names: HashSet<&str> = api
380            .types
381            .iter()
382            .filter(|t| !t.is_trait)
383            .map(|t| t.name.as_str())
384            .chain(api.enums.iter().map(|e| e.name.as_str()))
385            .collect();
386        for bridge_cfg in &config.trait_bridges {
387            if bridge_cfg.exclude_languages.contains(&Language::Java.to_string()) {
388                continue;
389            }
390
391            // When visitor_callbacks is active, visitor traits bound via options_field are
392            // surfaced through Visitor.java + VisitorBridge.java (generated by gen_visitor_files).
393            // The raw trait bridge I{Trait}.java emitted here would be an unreferenced orphan
394            // with snake_case method names. Suppress it for options_field-bound visitor traits.
395            if has_visitor_pattern && bridge_cfg.bind_via == BridgeBinding::OptionsField {
396                continue;
397            }
398
399            if let Some(trait_def) = api.types.iter().find(|t| t.name == bridge_cfg.trait_name && t.is_trait) {
400                let has_super_trait = bridge_cfg.super_trait.is_some();
401                let trait_bridge::BridgeFiles {
402                    interface_content,
403                    bridge_content,
404                } = trait_bridge::gen_trait_bridge_files(
405                    trait_def,
406                    &prefix,
407                    &package,
408                    has_super_trait,
409                    bridge_cfg.unregister_fn.as_deref(),
410                    bridge_cfg.clear_fn.as_deref(),
411                    &visible_type_names,
412                );
413
414                files.push(GeneratedFile {
415                    path: base_path.join(format!("I{}.java", trait_def.name)),
416                    content: interface_content,
417                    generated_header: true,
418                });
419                files.push(GeneratedFile {
420                    path: base_path.join(format!("{}Bridge.java", trait_def.name)),
421                    content: bridge_content,
422                    generated_header: true,
423                });
424            }
425        }
426
427        // Apply downstream Checkstyle line-length wrapping to every generated
428        // Java source. The templates emit some compound statements on one line;
429        // this pass splits at logical points (annotation lists, call args,
430        // method signatures) without changing semantics.
431        for file in &mut files {
432            file.content = line_wrap::wrap_long_java_lines(&file.content);
433        }
434
435        Ok(files)
436    }
437
438    fn generate_public_api(
439        &self,
440        api: &ApiSurface,
441        config: &ResolvedCrateConfig,
442    ) -> anyhow::Result<Vec<GeneratedFile>> {
443        let package = config.java_package();
444        let prefix = config.ffi_prefix();
445        let main_class = Self::resolve_main_class(api);
446        let package_path = package.replace('.', "/");
447
448        let output_dir = config
449            .output_for("java")
450            .map(|p| p.to_string_lossy().into_owned())
451            .unwrap_or_else(|| "packages/java/src/main/java/".to_string());
452
453        // If output_dir already ends with the package path (user configured the full path),
454        // use it as-is. Otherwise, append the package path.
455        let base_path = if output_dir.ends_with(&package_path) || output_dir.ends_with(&format!("{}/", package_path)) {
456            PathBuf::from(&output_dir)
457        } else {
458            PathBuf::from(&output_dir).join(&package_path)
459        };
460
461        // Collect bridge param names/aliases to strip from the public facade.
462        let bridge_param_names: HashSet<String> = config
463            .trait_bridges
464            .iter()
465            .filter_map(|b| b.param_name.clone())
466            .collect();
467        let bridge_type_aliases: HashSet<String> = config
468            .trait_bridges
469            .iter()
470            .filter_map(|b| b.type_alias.clone())
471            .collect();
472        let has_visitor_pattern = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false)
473            || config
474                .trait_bridges
475                .iter()
476                .any(|b| b.bind_via == BridgeBinding::OptionsField);
477        // Generate a high-level public API class that wraps the raw FFI class.
478        // Class name = main_class without "Rs" suffix (e.g., HtmlToMarkdownRs -> HtmlToMarkdown)
479        let public_class = main_class.trim_end_matches("Rs").to_string();
480        let facade_content = gen_facade_class(
481            api,
482            &package,
483            &public_class,
484            &main_class,
485            &prefix,
486            &bridge_param_names,
487            &bridge_type_aliases,
488            has_visitor_pattern,
489        );
490
491        Ok(vec![GeneratedFile {
492            path: base_path.join(format!("{}.java", public_class)),
493            content: line_wrap::wrap_long_java_lines(&facade_content),
494            generated_header: true,
495        }])
496    }
497
498    fn build_config(&self) -> Option<BuildConfig> {
499        Some(BuildConfig {
500            tool: "mvn",
501            crate_suffix: "",
502            build_dep: BuildDependency::Ffi,
503            post_build: vec![],
504        })
505    }
506}