use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::ApiSurface;
use crate::core::template_versions::{maven, toolchain};
use crate::{scaffold::parse_author, scaffold::scaffold_meta, scaffold::xml_escape};
use minijinja::context;
use std::path::PathBuf;
fn java_capsule_dependencies(config: &ResolvedCrateConfig) -> String {
let mut deps: Vec<(String, String)> = config
.java
.as_ref()
.map(|c| {
c.capsule_types
.values()
.filter(|cap| !cap.package.is_empty())
.map(|cap| (cap.package.clone(), cap.package_version.clone()))
.collect()
})
.unwrap_or_default();
deps.sort();
deps.dedup();
deps.iter()
.map(|(coord, ver)| {
let (group_id, artifact_id) = coord.split_once(':').unwrap_or((coord.as_str(), ""));
format!(
"\n <dependency>\n <groupId>{group_id}</groupId>\n \
<artifactId>{artifact_id}</artifactId>\n <version>{ver}</version>\n </dependency>"
)
})
.collect()
}
pub(crate) fn scaffold_java(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let meta = scaffold_meta(config);
let name = config.java_artifact_id();
let name = name.as_str();
let version = &api.version;
let repo_url = meta.configured_repository.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Java scaffold requires package metadata repository; set package_metadata.repository or scaffold.repository"
)
})?;
if meta.authors.is_empty() {
anyhow::bail!(
"Java scaffold requires package metadata authors; set package_metadata.authors or scaffold.authors"
);
}
let license = meta.license.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Java scaffold requires package metadata license; set package_metadata.license or scaffold.license"
)
})?;
let scm = scm_urls(repo_url);
let group_id = config.java_group_id();
let source_root = group_id.split('.').next().unwrap_or("dev");
let developers_xml = if meta.authors.is_empty() {
String::new()
} else {
let devs: Vec<String> = meta
.authors
.iter()
.map(|a| {
let (name, email) = parse_author(a);
let name_escaped = xml_escape(name);
let email_line = if email.is_empty() {
String::new()
} else {
format!("\n <email>{}</email>", xml_escape(email))
};
format!(
" <developer>\n <name>{name_escaped}</name>{email_line}\n </developer>"
)
})
.collect();
format!("\n <developers>\n{}\n </developers>\n", devs.join("\n"))
};
let license_url = match license {
"Elastic-2.0" => "https://www.elastic.co/licensing/elastic-license",
"MIT" => "https://opensource.org/licenses/MIT",
"Apache-2.0" => "https://www.apache.org/licenses/LICENSE-2.0",
_ => "",
};
let license_url_xml = if license_url.is_empty() {
String::new()
} else {
format!("\n <url>{license_url}</url>")
};
let content = crate::scaffold::template_env::render(
"java_pom.xml.jinja",
context! {
group_id => group_id,
name => name,
version => version,
description => meta.description,
repository => repo_url,
license => license,
license_url => license_url_xml,
developers => developers_xml,
scm_connection => scm.connection,
scm_developer_connection => scm.developer_connection,
capsule_deps => java_capsule_dependencies(config),
source_root => source_root,
java_release => toolchain::JAVA_JVM_TARGET,
junit_version => maven::JUNIT,
maven_core_version => maven::MAVEN_CORE,
maven_compiler_plugin_version => maven::MAVEN_COMPILER_PLUGIN,
maven_surefire_plugin_version => maven::MAVEN_SUREFIRE_PLUGIN,
maven_checkstyle_plugin_version => maven::MAVEN_CHECKSTYLE_PLUGIN,
maven_source_plugin_version => maven::MAVEN_SOURCE_PLUGIN,
maven_javadoc_plugin_version => maven::MAVEN_JAVADOC_PLUGIN,
maven_gpg_plugin_version => maven::MAVEN_GPG_PLUGIN,
maven_clean_plugin_version => maven::MAVEN_CLEAN_PLUGIN,
maven_resources_plugin_version => maven::MAVEN_RESOURCES_PLUGIN,
maven_jar_plugin_version => maven::MAVEN_JAR_PLUGIN,
maven_install_plugin_version => maven::MAVEN_INSTALL_PLUGIN,
maven_deploy_plugin_version => maven::MAVEN_DEPLOY_PLUGIN,
maven_site_plugin_version => maven::MAVEN_SITE_PLUGIN,
central_publishing_plugin_version => maven::CENTRAL_PUBLISHING_PLUGIN,
versions_maven_plugin_version => maven::VERSIONS_MAVEN_PLUGIN,
maven_enforcer_plugin_version => maven::MAVEN_ENFORCER_PLUGIN,
jacoco_maven_plugin_version => maven::JACOCO_MAVEN_PLUGIN,
checkstyle_version => maven::CHECKSTYLE,
jspecify_version => maven::JSPECIFY,
jackson_version => maven::JACKSON,
assertj_version => maven::ASSERTJ,
},
);
let checkstyle_xml = r#"<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<!-- Checkstyle handles correctness checks only. Formatting is handled by poly. -->
<module name="Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="error"/>
<property name="fileExtensions" value="java"/>
<module name="SuppressionFilter">
<property name="file" value="checkstyle-suppressions.xml"/>
<property name="optional" value="true"/>
</module>
<module name="LineLength">
<!-- 200 accommodates the alef-emitted DefaultClient.java FFM call shims:
the codegen chains arena allocation, MemorySegment marshalling, and
error-result handling onto single lines that don't reflow cleanly.
Tests and hand-written code stay well below this; the limit only
gives the generator headroom. -->
<property name="max" value="200"/>
<property name="ignorePattern" value="^package.*|^import.*|a]href|href|http://|https://|ftp://"/>
</module>
<module name="TreeWalker">
<!-- Naming Conventions (relaxed for FFI snake_case from Rust) -->
<module name="ConstantName">
<property name="format" value="^([A-Z][A-Z0-9]*(_[A-Z0-9]+)*|[a-z][a-zA-Z0-9_]*)$"/>
</module>
<module name="PackageName"/>
<module name="TypeName"/>
<!-- Modifier Checks -->
<module name="ModifierOrder"/>
<module name="RedundantModifier"/>
<!-- Imports -->
<module name="UnusedImports"/>
<!-- Coding -->
<module name="EmptyStatement"/>
<module name="EqualsHashCode"/>
<module name="SimplifyBooleanExpression"/>
<module name="SimplifyBooleanReturn"/>
<!-- Size Violations -->
<module name="MethodLength">
<property name="max" value="150"/>
</module>
<!-- Misc -->
<module name="ArrayTypeStyle"/>
<module name="UpperEll"/>
</module>
</module>
"#;
let checkstyle_properties = "";
let checkstyle_suppressions_xml = r#"<?xml version="1.0"?>
<!DOCTYPE suppressions PUBLIC
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
"https://checkstyle.org/dtds/suppressions_1_2.dtd">
<suppressions>
<!-- FFI constants -->
<suppress checks="ConstantName" files=".*FFI\.java"/>
<suppress checks="MagicNumber" files=".*FFI\.java"/>
<!-- Allow star imports and magic numbers in test files -->
<suppress checks="AvoidStarImport" files=".*Test\.java"/>
<suppress checks="MagicNumber" files=".*Test\.java"/>
<suppress checks="MethodLength" files=".*Test\.java"/>
</suppressions>
"#;
Ok(vec.*</ignoreVersion>
<ignoreVersion type="regex">(?i).*[.-]m\d+.*</ignoreVersion>
</ignoreVersions>
</ruleset>
"#
.to_string(),
generated_header: false,
},
])
}
struct ScmUrls {
connection: String,
developer_connection: String,
}
fn scm_urls(repository: &str) -> ScmUrls {
let normalized = repository.trim_end_matches(".git");
let without_scheme = normalized
.strip_prefix("https://")
.or_else(|| normalized.strip_prefix("http://"))
.unwrap_or(normalized);
let (host, path) = without_scheme.split_once('/').unwrap_or((without_scheme, ""));
let suffix = if path.is_empty() {
String::new()
} else {
format!("/{path}.git")
};
ScmUrls {
connection: format!("scm:git:git://{host}{suffix}"),
developer_connection: format!("scm:git:ssh://git@{host}{suffix}"),
}
}