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 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 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 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 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 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 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 for enum_def in &api.enums {
332 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 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 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, });
378 }
379 }
380
381 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 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 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 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 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 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}