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, gen_json_util_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 files.push(GeneratedFile {
312 path: base_path.join("JsonUtil.java"),
313 content: gen_json_util_class(&package, &main_class),
314 generated_header: true,
315 });
316
317 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
319 if typ.is_opaque {
320 files.push(GeneratedFile {
321 path: base_path.join(format!("{}.java", typ.name)),
322 content: gen_opaque_handle_class(&package, typ, &prefix, &config.adapters, &main_class),
323 generated_header: true,
324 });
325 }
326 }
327
328 for enum_def in &api.enums {
330 if has_visitor_pattern && bridge_associated_types.contains(enum_def.name.as_str()) {
332 continue;
333 }
334 files.push(GeneratedFile {
335 path: base_path.join(format!("{}.java", enum_def.name)),
336 content: gen_enum_class(&package, enum_def, &main_class),
337 generated_header: true,
338 });
339 }
340
341 let infrastructure_exception_names: AHashSet<&str> = ["InvalidInputException", "ConversionErrorException"]
349 .into_iter()
350 .collect();
351 let mut emitted_exception_names: AHashSet<String> = AHashSet::new();
352 for error in &api.errors {
353 for (class_name, content) in alef_codegen::error_gen::gen_java_error_types(error, &package) {
354 if infrastructure_exception_names.contains(class_name.as_str()) {
355 continue;
356 }
357 if !emitted_exception_names.insert(class_name.clone()) {
358 continue;
359 }
360 files.push(GeneratedFile {
361 path: base_path.join(format!("{}.java", class_name)),
362 content,
363 generated_header: true,
364 });
365 }
366 }
367
368 if has_visitor_pattern {
370 for (filename, content) in crate::gen_visitor::gen_visitor_files(&package, &main_class) {
371 files.push(GeneratedFile {
372 path: base_path.join(filename),
373 content,
374 generated_header: false, });
376 }
377 }
378
379 let visible_type_names: HashSet<&str> = api
387 .types
388 .iter()
389 .filter(|t| !t.is_trait)
390 .map(|t| t.name.as_str())
391 .chain(api.enums.iter().map(|e| e.name.as_str()))
392 .collect();
393 for bridge_cfg in &config.trait_bridges {
394 if bridge_cfg.exclude_languages.contains(&Language::Java.to_string()) {
395 continue;
396 }
397
398 if has_visitor_pattern && bridge_cfg.bind_via == BridgeBinding::OptionsField {
403 continue;
404 }
405
406 if let Some(trait_def) = api.types.iter().find(|t| t.name == bridge_cfg.trait_name && t.is_trait) {
407 let has_super_trait = bridge_cfg.super_trait.is_some();
408 let trait_bridge::BridgeFiles {
409 interface_content,
410 bridge_content,
411 } = trait_bridge::gen_trait_bridge_files(
412 trait_def,
413 &prefix,
414 &package,
415 has_super_trait,
416 bridge_cfg.unregister_fn.as_deref(),
417 bridge_cfg.clear_fn.as_deref(),
418 &visible_type_names,
419 );
420
421 files.push(GeneratedFile {
422 path: base_path.join(format!("I{}.java", trait_def.name)),
423 content: interface_content,
424 generated_header: true,
425 });
426 files.push(GeneratedFile {
427 path: base_path.join(format!("{}Bridge.java", trait_def.name)),
428 content: bridge_content,
429 generated_header: true,
430 });
431 }
432 }
433
434 for file in &mut files {
439 file.content = line_wrap::wrap_long_java_lines(&file.content);
440 }
441
442 Ok(files)
443 }
444
445 fn generate_public_api(
446 &self,
447 api: &ApiSurface,
448 config: &ResolvedCrateConfig,
449 ) -> anyhow::Result<Vec<GeneratedFile>> {
450 let package = config.java_package();
451 let prefix = config.ffi_prefix();
452 let main_class = Self::resolve_main_class(api);
453 let package_path = package.replace('.', "/");
454
455 let output_dir = config
456 .output_for("java")
457 .map(|p| p.to_string_lossy().into_owned())
458 .unwrap_or_else(|| "packages/java/src/main/java/".to_string());
459
460 let base_path = if output_dir.ends_with(&package_path) || output_dir.ends_with(&format!("{}/", package_path)) {
463 PathBuf::from(&output_dir)
464 } else {
465 PathBuf::from(&output_dir).join(&package_path)
466 };
467
468 let bridge_param_names: HashSet<String> = config
470 .trait_bridges
471 .iter()
472 .filter_map(|b| b.param_name.clone())
473 .collect();
474 let bridge_type_aliases: HashSet<String> = config
475 .trait_bridges
476 .iter()
477 .filter_map(|b| b.type_alias.clone())
478 .collect();
479 let has_visitor_pattern = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false)
480 || config
481 .trait_bridges
482 .iter()
483 .any(|b| b.bind_via == BridgeBinding::OptionsField);
484 let public_class = main_class.trim_end_matches("Rs").to_string();
487 let facade_content = gen_facade_class(
488 api,
489 &package,
490 &public_class,
491 &main_class,
492 &prefix,
493 &bridge_param_names,
494 &bridge_type_aliases,
495 has_visitor_pattern,
496 );
497
498 Ok(vec![GeneratedFile {
499 path: base_path.join(format!("{}.java", public_class)),
500 content: line_wrap::wrap_long_java_lines(&facade_content),
501 generated_header: true,
502 }])
503 }
504
505 fn build_config(&self) -> Option<BuildConfig> {
506 Some(BuildConfig {
507 tool: "mvn",
508 crate_suffix: "",
509 build_dep: BuildDependency::Ffi,
510 post_build: vec![],
511 })
512 }
513}