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_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 }
287 }
288
289 let needs_bytes_serializer = api
293 .types
294 .iter()
295 .any(|t| !t.is_opaque && t.fields.iter().any(|f| !f.optional && matches!(f.ty, TypeRef::Bytes)));
296 if needs_bytes_serializer {
297 files.push(GeneratedFile {
298 path: base_path.join("ByteArrayToIntArraySerializer.java"),
299 content: gen_byte_array_serializer(&package),
300 generated_header: true,
301 });
302 }
303
304 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
306 if typ.is_opaque {
307 files.push(GeneratedFile {
308 path: base_path.join(format!("{}.java", typ.name)),
309 content: gen_opaque_handle_class(&package, typ, &prefix, &config.adapters, &main_class),
310 generated_header: true,
311 });
312 }
313 }
314
315 for enum_def in &api.enums {
317 if has_visitor_pattern && bridge_associated_types.contains(enum_def.name.as_str()) {
319 continue;
320 }
321 files.push(GeneratedFile {
322 path: base_path.join(format!("{}.java", enum_def.name)),
323 content: gen_enum_class(&package, enum_def, &main_class),
324 generated_header: true,
325 });
326 }
327
328 let infrastructure_exception_names: AHashSet<&str> = ["InvalidInputException", "ConversionErrorException"]
336 .into_iter()
337 .collect();
338 let mut emitted_exception_names: AHashSet<String> = AHashSet::new();
339 for error in &api.errors {
340 for (class_name, content) in alef_codegen::error_gen::gen_java_error_types(error, &package) {
341 if infrastructure_exception_names.contains(class_name.as_str()) {
342 continue;
343 }
344 if !emitted_exception_names.insert(class_name.clone()) {
345 continue;
346 }
347 files.push(GeneratedFile {
348 path: base_path.join(format!("{}.java", class_name)),
349 content,
350 generated_header: true,
351 });
352 }
353 }
354
355 if has_visitor_pattern {
357 for (filename, content) in crate::gen_visitor::gen_visitor_files(&package, &main_class) {
358 files.push(GeneratedFile {
359 path: base_path.join(filename),
360 content,
361 generated_header: false, });
363 }
364 }
365
366 let visible_type_names: HashSet<&str> = api
374 .types
375 .iter()
376 .filter(|t| !t.is_trait)
377 .map(|t| t.name.as_str())
378 .chain(api.enums.iter().map(|e| e.name.as_str()))
379 .collect();
380 for bridge_cfg in &config.trait_bridges {
381 if bridge_cfg.exclude_languages.contains(&Language::Java.to_string()) {
382 continue;
383 }
384
385 if has_visitor_pattern && bridge_cfg.bind_via == BridgeBinding::OptionsField {
390 continue;
391 }
392
393 if let Some(trait_def) = api.types.iter().find(|t| t.name == bridge_cfg.trait_name && t.is_trait) {
394 let has_super_trait = bridge_cfg.super_trait.is_some();
395 let trait_bridge::BridgeFiles {
396 interface_content,
397 bridge_content,
398 } = trait_bridge::gen_trait_bridge_files(
399 trait_def,
400 &prefix,
401 &package,
402 has_super_trait,
403 bridge_cfg.unregister_fn.as_deref(),
404 bridge_cfg.clear_fn.as_deref(),
405 &visible_type_names,
406 );
407
408 files.push(GeneratedFile {
409 path: base_path.join(format!("I{}.java", trait_def.name)),
410 content: interface_content,
411 generated_header: true,
412 });
413 files.push(GeneratedFile {
414 path: base_path.join(format!("{}Bridge.java", trait_def.name)),
415 content: bridge_content,
416 generated_header: true,
417 });
418 }
419 }
420
421 for file in &mut files {
426 file.content = line_wrap::wrap_long_java_lines(&file.content);
427 }
428
429 Ok(files)
430 }
431
432 fn generate_public_api(
433 &self,
434 api: &ApiSurface,
435 config: &ResolvedCrateConfig,
436 ) -> anyhow::Result<Vec<GeneratedFile>> {
437 let package = config.java_package();
438 let prefix = config.ffi_prefix();
439 let main_class = Self::resolve_main_class(api);
440 let package_path = package.replace('.', "/");
441
442 let output_dir = config
443 .output_for("java")
444 .map(|p| p.to_string_lossy().into_owned())
445 .unwrap_or_else(|| "packages/java/src/main/java/".to_string());
446
447 let base_path = if output_dir.ends_with(&package_path) || output_dir.ends_with(&format!("{}/", package_path)) {
450 PathBuf::from(&output_dir)
451 } else {
452 PathBuf::from(&output_dir).join(&package_path)
453 };
454
455 let bridge_param_names: HashSet<String> = config
457 .trait_bridges
458 .iter()
459 .filter_map(|b| b.param_name.clone())
460 .collect();
461 let bridge_type_aliases: HashSet<String> = config
462 .trait_bridges
463 .iter()
464 .filter_map(|b| b.type_alias.clone())
465 .collect();
466 let has_visitor_pattern = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false)
467 || config
468 .trait_bridges
469 .iter()
470 .any(|b| b.bind_via == BridgeBinding::OptionsField);
471 let public_class = main_class.trim_end_matches("Rs").to_string();
474 let facade_content = gen_facade_class(
475 api,
476 &package,
477 &public_class,
478 &main_class,
479 &prefix,
480 &bridge_param_names,
481 &bridge_type_aliases,
482 has_visitor_pattern,
483 );
484
485 Ok(vec![GeneratedFile {
486 path: base_path.join(format!("{}.java", public_class)),
487 content: line_wrap::wrap_long_java_lines(&facade_content),
488 generated_header: true,
489 }])
490 }
491
492 fn build_config(&self) -> Option<BuildConfig> {
493 Some(BuildConfig {
494 tool: "mvn",
495 crate_suffix: "",
496 build_dep: BuildDependency::Ffi,
497 post_build: vec![],
498 })
499 }
500}