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, 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_builder_class, 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                files.push(GeneratedFile {
272                    path: base_path.join(format!("{}.java", typ.name)),
273                    content: gen_record_type(
274                        &package,
275                        typ,
276                        &complex_enums,
277                        &sealed_unions_with_unwrapped,
278                        &lang_rename_all,
279                        has_visitor_pattern,
280                        &main_class,
281                    ),
282                    generated_header: true,
283                });
284                // Generate builder class for types with defaults
285                if typ.has_default {
286                    files.push(GeneratedFile {
287                        path: base_path.join(format!("{}Builder.java", typ.name)),
288                        content: gen_builder_class(&package, typ, has_visitor_pattern),
289                        generated_header: true,
290                    });
291                }
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        // Collect builder class names generated from record types with defaults,
311        // so we can skip opaque types that would collide with them.
312        let builder_class_names: AHashSet<String> = api
313            .types
314            .iter()
315            .filter(|t| !t.is_opaque && (!t.fields.is_empty() || (t.has_serde && t.fields.is_empty())) && t.has_default)
316            .map(|t| format!("{}Builder", t.name))
317            .collect();
318
319        // 4b. Opaque handle types (skip if a pure-Java builder already covers this name)
320        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
321            if typ.is_opaque && !builder_class_names.contains(&typ.name) {
322                files.push(GeneratedFile {
323                    path: base_path.join(format!("{}.java", typ.name)),
324                    content: gen_opaque_handle_class(&package, typ, &prefix, &config.adapters, &main_class),
325                    generated_header: true,
326                });
327            }
328        }
329
330        // 5. Enums
331        for enum_def in &api.enums {
332            // Skip enums that gen_visitor handles with richer visitor-specific versions
333            if has_visitor_pattern && bridge_associated_types.contains(enum_def.name.as_str()) {
334                continue;
335            }
336            files.push(GeneratedFile {
337                path: base_path.join(format!("{}.java", enum_def.name)),
338                content: gen_enum_class(&package, enum_def, &main_class),
339                generated_header: true,
340            });
341        }
342
343        // 6. Error exception classes
344        //
345        // Filter out variants whose generated class name collides with the FFI infrastructure
346        // exceptions emitted at step 3b. Both paths target the same .java file; without this
347        // filter, the gen_java_error_types content was overwriting (or worse, mangling — the
348        // InvalidInputException file ended up with a duplicate constructor block appended
349        // after the closing brace) the canonical infrastructure-emitted class.
350        let infrastructure_exception_names: AHashSet<&str> = ["InvalidInputException", "ConversionErrorException"]
351            .into_iter()
352            .collect();
353        let mut emitted_exception_names: AHashSet<String> = AHashSet::new();
354        for error in &api.errors {
355            for (class_name, content) in alef_codegen::error_gen::gen_java_error_types(error, &package) {
356                if infrastructure_exception_names.contains(class_name.as_str()) {
357                    continue;
358                }
359                if !emitted_exception_names.insert(class_name.clone()) {
360                    continue;
361                }
362                files.push(GeneratedFile {
363                    path: base_path.join(format!("{}.java", class_name)),
364                    content,
365                    generated_header: true,
366                });
367            }
368        }
369
370        // 7. Visitor support files (only when ConversionOptions/ConversionResult types exist)
371        if has_visitor_pattern {
372            for (filename, content) in crate::gen_visitor::gen_visitor_files(&package, &main_class) {
373                files.push(GeneratedFile {
374                    path: base_path.join(filename),
375                    content,
376                    generated_header: false, // already has header comment
377                });
378            }
379        }
380
381        // 8. Trait bridge plugin registration files
382        // Emits two files per trait: I{Trait}.java (managed interface) and
383        // {Trait}Bridge.java (Panama upcall stubs + register/unregister helpers).
384        //
385        // Set of struct + enum names that get a generated companion Java class.
386        // Trait method signatures referencing types outside this set (e.g. excluded
387        // internal types like `InternalDocument`) are JSON-bridged as Strings.
388        let visible_type_names: HashSet<&str> = api
389            .types
390            .iter()
391            .filter(|t| !t.is_trait)
392            .map(|t| t.name.as_str())
393            .chain(api.enums.iter().map(|e| e.name.as_str()))
394            .collect();
395        for bridge_cfg in &config.trait_bridges {
396            if bridge_cfg.exclude_languages.contains(&Language::Java.to_string()) {
397                continue;
398            }
399
400            // When visitor_callbacks is active, visitor traits bound via options_field are
401            // surfaced through Visitor.java + VisitorBridge.java (generated by gen_visitor_files).
402            // The raw trait bridge I{Trait}.java emitted here would be an unreferenced orphan
403            // with snake_case method names. Suppress it for options_field-bound visitor traits.
404            if has_visitor_pattern && bridge_cfg.bind_via == BridgeBinding::OptionsField {
405                continue;
406            }
407
408            if let Some(trait_def) = api.types.iter().find(|t| t.name == bridge_cfg.trait_name && t.is_trait) {
409                let has_super_trait = bridge_cfg.super_trait.is_some();
410                let trait_bridge::BridgeFiles {
411                    interface_content,
412                    bridge_content,
413                } = trait_bridge::gen_trait_bridge_files(
414                    trait_def,
415                    &prefix,
416                    &package,
417                    has_super_trait,
418                    bridge_cfg.unregister_fn.as_deref(),
419                    bridge_cfg.clear_fn.as_deref(),
420                    &visible_type_names,
421                );
422
423                files.push(GeneratedFile {
424                    path: base_path.join(format!("I{}.java", trait_def.name)),
425                    content: interface_content,
426                    generated_header: true,
427                });
428                files.push(GeneratedFile {
429                    path: base_path.join(format!("{}Bridge.java", trait_def.name)),
430                    content: bridge_content,
431                    generated_header: true,
432                });
433            }
434        }
435
436        // Apply downstream Checkstyle line-length wrapping to every generated
437        // Java source. The templates emit some compound statements on one line;
438        // this pass splits at logical points (annotation lists, call args,
439        // method signatures) without changing semantics.
440        for file in &mut files {
441            file.content = line_wrap::wrap_long_java_lines(&file.content);
442        }
443
444        Ok(files)
445    }
446
447    fn generate_public_api(
448        &self,
449        api: &ApiSurface,
450        config: &ResolvedCrateConfig,
451    ) -> anyhow::Result<Vec<GeneratedFile>> {
452        let package = config.java_package();
453        let prefix = config.ffi_prefix();
454        let main_class = Self::resolve_main_class(api);
455        let package_path = package.replace('.', "/");
456
457        let output_dir = config
458            .output_for("java")
459            .map(|p| p.to_string_lossy().into_owned())
460            .unwrap_or_else(|| "packages/java/src/main/java/".to_string());
461
462        // If output_dir already ends with the package path (user configured the full path),
463        // use it as-is. Otherwise, append the package path.
464        let base_path = if output_dir.ends_with(&package_path) || output_dir.ends_with(&format!("{}/", package_path)) {
465            PathBuf::from(&output_dir)
466        } else {
467            PathBuf::from(&output_dir).join(&package_path)
468        };
469
470        // Collect bridge param names/aliases to strip from the public facade.
471        let bridge_param_names: HashSet<String> = config
472            .trait_bridges
473            .iter()
474            .filter_map(|b| b.param_name.clone())
475            .collect();
476        let bridge_type_aliases: HashSet<String> = config
477            .trait_bridges
478            .iter()
479            .filter_map(|b| b.type_alias.clone())
480            .collect();
481        let has_visitor_pattern = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false)
482            || config
483                .trait_bridges
484                .iter()
485                .any(|b| b.bind_via == BridgeBinding::OptionsField);
486        // Generate a high-level public API class that wraps the raw FFI class.
487        // Class name = main_class without "Rs" suffix (e.g., HtmlToMarkdownRs -> HtmlToMarkdown)
488        let public_class = main_class.trim_end_matches("Rs").to_string();
489        let facade_content = gen_facade_class(
490            api,
491            &package,
492            &public_class,
493            &main_class,
494            &prefix,
495            &bridge_param_names,
496            &bridge_type_aliases,
497            has_visitor_pattern,
498        );
499
500        Ok(vec![GeneratedFile {
501            path: base_path.join(format!("{}.java", public_class)),
502            content: line_wrap::wrap_long_java_lines(&facade_content),
503            generated_header: true,
504        }])
505    }
506
507    fn build_config(&self) -> Option<BuildConfig> {
508        Some(BuildConfig {
509            tool: "mvn",
510            crate_suffix: "",
511            build_dep: BuildDependency::Ffi,
512            post_build: vec![],
513        })
514    }
515}