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 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(¶m.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 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 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 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 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 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 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 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 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 let complex_enums: AHashSet<String> = AHashSet::new();
240
241 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 let lang_rename_all = config.serde_rename_all_for_language(Language::Java);
258
259 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 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 }
293 }
294
295 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 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 for enum_def in &api.enums {
323 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 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 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, });
369 }
370 }
371
372 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 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 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 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 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 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}