use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::ApiSurface;
use crate::{scaffold::parse_author, scaffold::scaffold_meta, scaffold::xml_escape};
use std::path::PathBuf;
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 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 = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>{group_id}</groupId>
<artifactId>{name}</artifactId>
<version>{version}</version>
<packaging>jar</packaging>
<name>{name}</name>
<description>{description}</description>
<url>{repository}</url>
<licenses>
<license>
<name>{license}</name>{license_url}
</license>
</licenses>
{developers}
<scm>
<connection>{scm_connection}</connection>
<developerConnection>{scm_developer_connection}</developerConnection>
<url>{repository}</url>
</scm>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>25</maven.compiler.release>
<junit.version>5.11.4</junit.version>
<maven.version>3.9.11</maven.version>
<maven-compiler-plugin.version>3.15.0</maven-compiler-plugin.version>
<maven-surefire-plugin.version>3.5.5</maven-surefire-plugin.version>
<maven-checkstyle-plugin.version>3.6.0</maven-checkstyle-plugin.version>
<maven-pmd-plugin.version>3.28.0</maven-pmd-plugin.version>
<maven-source-plugin.version>3.4.0</maven-source-plugin.version>
<maven-javadoc-plugin.version>3.12.0</maven-javadoc-plugin.version>
<maven-gpg-plugin.version>3.2.8</maven-gpg-plugin.version>
<maven-clean-plugin.version>3.4.1</maven-clean-plugin.version>
<maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
<maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
<maven-install-plugin.version>3.1.3</maven-install-plugin.version>
<maven-deploy-plugin.version>3.1.3</maven-deploy-plugin.version>
<maven-site-plugin.version>4.0.0-M16</maven-site-plugin.version>
<central-publishing-plugin.version>0.10.0</central-publishing-plugin.version>
<spotless-maven-plugin.version>3.4.0</spotless-maven-plugin.version>
<versions-maven-plugin.version>2.21.0</versions-maven-plugin.version>
<maven-enforcer-plugin.version>3.6.2</maven-enforcer-plugin.version>
<jacoco-maven-plugin.version>0.8.14</jacoco-maven-plugin.version>
<checkstyle.version>13.4.0</checkstyle.version>
<pmd.version>7.17.0</pmd.version>
<gpg.skip>true</gpg.skip>
</properties>
<dependencies>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.21.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.21.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${{junit.version}}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>4.0.0-M1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<!-- The alef Java backend emits source files at the package root
(e.g. packages/java/dev/<group>/<artifact>/Foo.java), not under
the Maven-default `src/main/java/` layout. Point sourceDirectory
at the package root so `mvn package` finds them. -->
<sourceDirectory>${{project.basedir}}</sourceDirectory>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>${{maven-clean-plugin.version}}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>${{maven-resources-plugin.version}}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>${{maven-jar-plugin.version}}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>${{maven-install-plugin.version}}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>${{maven-deploy-plugin.version}}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>${{maven-site-plugin.version}}</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${{maven-compiler-plugin.version}}</version>
<configuration>
<release>25</release>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${{maven-surefire-plugin.version}}</version>
<configuration>
<argLine>@{{argLine}} -XX:-ClassUnloading -XX:-ClassUnloadingWithConcurrentMark --enable-native-access=ALL-UNNAMED --enable-preview -Djava.library.path=${{project.basedir}}/../../target/release</argLine>
<forkedProcessExitTimeoutInSeconds>600</forkedProcessExitTimeoutInSeconds>
<parallel>classes</parallel>
<threadCount>4</threadCount>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
</configuration>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>${{spotless-maven-plugin.version}}</version>
<configuration>
<java>
<eclipse>
<version>4.31</version>
<file>${{project.basedir}}/eclipse-formatter.xml</file>
</eclipse>
</java>
</configuration>
<executions>
<execution>
<goals>
<goal>apply</goal>
</goals>
<phase>process-sources</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>${{maven-source-plugin.version}}</version>
<configuration>
<!-- sourceDirectory is the project basedir, so the default
source-archive include of everything under basedir
pulls in target/ as well (which contains the archive
being assembled — "A zip file cannot include itself").
Restrict to the alef-emitted dev/ subtree and any
conventional `src/main/java/` overlay. -->
<includes>
<include>dev/**/*.java</include>
<include>src/main/java/**/*.java</include>
</includes>
</configuration>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${{maven-javadoc-plugin.version}}</version>
<configuration>
<doclint>all,-missing</doclint>
<failOnWarning>true</failOnWarning>
<show>protected</show>
<additionalOptions>--enable-preview</additionalOptions>
<!-- sourcepath MUST match <sourceDirectory> above (which is
${{project.basedir}} for the flat layout alef emits) — the
Maven-default `src/main/java/` does not exist in our tree,
so attach-javadocs found no sources and skipped jar
creation, which Sonatype Central rejected as
"Javadocs must be provided but not found in entries". -->
<sourcepath>${{project.basedir}}</sourcepath>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>${{maven-enforcer-plugin.version}}</version>
<executions>
<execution>
<id>enforce-maven</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireMavenVersion>
<version>${{maven.version}}</version>
</requireMavenVersion>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${{maven-checkstyle-plugin.version}}</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>${{checkstyle.version}}</version>
</dependency>
</dependencies>
<configuration>
<configLocation>${{project.basedir}}/checkstyle.xml</configLocation>
<propertiesLocation>${{project.basedir}}/checkstyle.properties</propertiesLocation>
<consoleOutput>true</consoleOutput>
<failsOnError>true</failsOnError>
<violationSeverity>warning</violationSeverity>
<propertyExpansion>config_loc=${{project.basedir}}</propertyExpansion>
</configuration>
<executions>
<execution>
<id>validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
<version>${{maven-pmd-plugin.version}}</version>
<dependencies>
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-java</artifactId>
<version>${{pmd.version}}</version>
</dependency>
</dependencies>
<configuration>
<targetJdk>${{maven.compiler.release}}</targetJdk>
<typeResolution>false</typeResolution>
<rulesets>
<ruleset>/rulesets/java/quickstart.xml</ruleset>
</rulesets>
<!--
CPD threshold raised above the default 100 tokens because alef-generated
streaming method bodies (`streamItems`, `batchStreamItems`, etc.) share
an identical iterator-driving loop by design (per-stream-handle JNI
externs differ, the surrounding plumbing is the same). The shared block
is ~106 tokens — well within the default. 200 is the smallest threshold
that lets two streaming methods coexist in the same handle class without
a false-positive while still catching genuine large-scale duplication.
-->
<minimumTokens>200</minimumTokens>
</configuration>
<executions>
<execution>
<goals>
<goal>pmd</goal>
<goal>cpd-check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>${{versions-maven-plugin.version}}</version>
<configuration>
<generateBackupPoms>false</generateBackupPoms>
<rulesUri>file://${{project.basedir}}/versions-rules.xml</rulesUri>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${{jacoco-maven-plugin.version}}</version>
<configuration>
<excludes>
<exclude>java/**/*</exclude>
<exclude>sun/**/*</exclude>
<exclude>jdk/**/*</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>${{maven-gpg-plugin.version}}</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>publish</id>
<properties>
<gpg.skip>false</gpg.skip>
<!-- alef-emitted stream methods can exceed 200 tokens and trigger CPD/PMD
duplicate-code violations; skip those checks in the publish profile so
they do not block Maven Central deployment. -->
<cpd.skip>true</cpd.skip>
<pmd.skip>true</pmd.skip>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>${{maven-gpg-plugin.version}}</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
<configuration>
<passphraseEnvName>MAVEN_GPG_PASSPHRASE</passphraseEnvName>
<gpgArguments>
<arg>--batch</arg>
<arg>--yes</arg>
<arg>--pinentry-mode=loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>${{central-publishing-plugin.version}}</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>ossrh</publishingServerId>
<autoPublish>true</autoPublish>
<waitUntil>published</waitUntil>
<waitMaxTime>7200</waitMaxTime>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
"#,
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,
);
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. Spotless handles all formatting. -->
<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>
"#;
let eclipse_formatter_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<profiles version="21">
<profile kind="CodeFormatterProfile" name="Alef" version="21">
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="140"/>
<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="140"/>
</profile>
</profiles>
"#;
Ok(vec.*</ignoreVersion>
<ignoreVersion type="regex">(?i).*[.-]m\d+.*</ignoreVersion>
</ignoreVersions>
</ruleset>
"#
.to_string(),
generated_header: false,
},
GeneratedFile {
path: PathBuf::from("packages/java/pmd-ruleset.xml"),
content: r#"<?xml version="1.0"?>
<ruleset name="Custom PMD Ruleset"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0
https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>PMD ruleset for Java bindings</description>
<rule ref="category/java/bestpractices.xml">
<exclude name="LooseCoupling"/>
</rule>
<rule ref="category/java/codestyle.xml">
<exclude name="AtLeastOneConstructor"/>
<exclude name="CommentDefaultAccessModifier"/>
<exclude name="OnlyOneReturn"/>
</rule>
<rule ref="category/java/design.xml">
<exclude name="LawOfDemeter"/>
<exclude name="DataClass"/>
</rule>
<rule ref="category/java/documentation.xml">
<exclude name="CommentSize"/>
</rule>
<rule ref="category/java/errorprone.xml">
<exclude name="EmptyCatchBlock"/>
</rule>
<rule ref="category/java/multithreading.xml"/>
<rule ref="category/java/performance.xml"/>
<rule ref="category/java/security.xml"/>
</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}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::NewAlefConfig;
use crate::core::ir::ApiSurface;
fn resolve_config(toml_text: &str) -> ResolvedCrateConfig {
let cfg: NewAlefConfig = toml::from_str(toml_text).expect("valid config");
cfg.resolve().expect("resolve").remove(0)
}
#[test]
fn pom_publish_profile_contains_cpd_and_pmd_skip() {
let config = resolve_config(
r#"
[workspace]
languages = ["java"]
[[crates]]
name = "testlib"
sources = []
[crates.package_metadata]
repository = "https://github.com/example/testlib"
authors = ["Test Author <test@example.com>"]
license = "MIT"
description = "A test library"
"#,
);
let api = ApiSurface::default();
let files = scaffold_java(&api, &config).expect("scaffold_java succeeds");
let pom = files
.iter()
.find(|f| f.path == *"packages/java/pom.xml")
.expect("pom.xml present");
assert!(
pom.content.contains("<cpd.skip>true</cpd.skip>"),
"pom.xml publish profile must contain <cpd.skip>true</cpd.skip>"
);
assert!(
pom.content.contains("<pmd.skip>true</pmd.skip>"),
"pom.xml publish profile must contain <pmd.skip>true</pmd.skip>"
);
}
}