From 908c4b51d764b81c3b71726a3fa77718faa495b3 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 11 Sep 2026 15:33:39 +0200 Subject: [PATCH 1/5] Improve code generation and release build Update IC4J agent and Candid dependencies to 0.8.5, correct generated Java, Spring, and React Native behavior, harden CLI and JAXB error handling, and add reproducible packaging and regression coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 26 ++++ .gitignore | 1 - DEPLOY.md | 27 ++-- README.md | 8 +- build.gradle | 114 ++++++++++++-- jbang/IC4JJBang.java | 14 +- .../org/ic4j/codegen/CodegenException.java | 4 + src/main/java/org/ic4j/codegen/IC4J.java | 16 +- src/main/java/org/ic4j/codegen/IC4JBase.java | 94 ++++++++---- .../java/org/ic4j/codegen/JAXBGenerator.java | 49 ++++-- .../ic4j/codegen/JakartaJAXBGenerator.java | 26 ++-- .../java/org/ic4j/codegen/JavaIdentifier.java | 74 +++++++++ .../java/org/ic4j/codegen/JavaWriter.java | 81 ++++------ .../org/ic4j/codegen/JavaWriterContext.java | 18 +++ .../org/ic4j/codegen/JavaxJAXBGenerator.java | 26 ++-- .../org/ic4j/codegen/ReactNativeWriter.java | 92 +++++------- .../java/org/ic4j/codegen/SpringWriter.java | 34 +++-- .../ic4j/codegen/GeneratorRegressionTest.java | 140 ++++++++++++++++++ .../org/ic4j/codegen/test/JAXBWriterTest.java | 2 + .../org/ic4j/codegen/test/RosettaNetTest.java | 1 + .../java/org/ic4j/codegen/test/SwiftTest.java | 1 + .../org/ic4j/codegen/test/XSDWriterTest.java | 2 + 22 files changed, 614 insertions(+), 236 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 src/main/java/org/ic4j/codegen/JavaIdentifier.java create mode 100644 src/test/java/org/ic4j/codegen/GeneratorRegressionTest.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..0d7f9c8 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,26 @@ +name: Build + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + java: [8, 11, 21] + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ matrix.java }} + cache: gradle + - uses: gradle/actions/setup-gradle@v4 + - run: ./gradlew check fatJar --console=plain diff --git a/.gitignore b/.gitignore index e5d857e..1cb70d9 100755 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ /.gradle/ /bin/ -/build-export.gradle /gradle.properties /.classpath /secring.gpg diff --git a/DEPLOY.md b/DEPLOY.md index c4060d5..0755b76 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -3,7 +3,7 @@ ## Prerequisites - Use JDK 21 for publishing tasks. -- `build-export.gradle` is the publishing build file. +- Publishing is configured in the tracked `build.gradle`. - Signing can come from `gradle.properties` or environment variables. - Central credentials can come from either: - `centralUsername` and `centralPassword` @@ -40,8 +40,7 @@ Run dry runs first to validate task wiring and credentials without uploading art ```bash source scripts/load-maven-env.sh -./gradlew -b build-export.gradle --no-daemon -PcentralUsername="$CENTRAL_PORTAL_USERNAME" -PcentralPassword="$CENTRAL_PORTAL_PASSWORD" publishAggregationToCentralPortal --dry-run --console=plain -./gradlew -b build-export.gradle --no-daemon -PcentralUsername="$CENTRAL_PORTAL_USERNAME" -PcentralPassword="$CENTRAL_PORTAL_PASSWORD" publishAggregationToCentralSnapshots --dry-run --console=plain +./gradlew --no-daemon publishToMavenLocal --dry-run --console=plain ``` ## Test Matrix @@ -80,13 +79,13 @@ source scripts/load-maven-env.sh ```bash source scripts/load-maven-env.sh -./gradlew -b build-export.gradle --no-daemon -PcentralUsername="$CENTRAL_PORTAL_USERNAME" -PcentralPassword="$CENTRAL_PORTAL_PASSWORD" publishToMavenLocal --console=plain +./gradlew --no-daemon publishToMavenLocal --console=plain ``` Artifacts are published under: ```bash -~/.m2/repository/org/ic4j/ic4j-codegen/0.8.0 +~/.m2/repository/org/ic4j/ic4j-codegen/0.8.5 ``` ## Central Portal Release @@ -94,21 +93,29 @@ Artifacts are published under: ```bash source scripts/load-maven-env.sh scripts/release-preflight.sh -./gradlew -b build-export.gradle --no-daemon -PcentralUsername="$CENTRAL_PORTAL_USERNAME" -PcentralPassword="$CENTRAL_PORTAL_PASSWORD" publishAggregationToCentralPortal --console=plain +./gradlew --no-daemon \ + -PreleaseRepositoryUrl="" \ + -PreleaseRepositoryUsername="$CENTRAL_PORTAL_USERNAME" \ + -PreleaseRepositoryPassword="$CENTRAL_PORTAL_PASSWORD" \ + publishMavenJavaPublicationToReleaseRepository --console=plain ``` ## Central Snapshots ```bash source scripts/load-maven-env.sh -./gradlew -b build-export.gradle --no-daemon -PcentralUsername="$CENTRAL_PORTAL_USERNAME" -PcentralPassword="$CENTRAL_PORTAL_PASSWORD" publishAggregationToCentralSnapshots --console=plain +./gradlew --no-daemon \ + -PreleaseRepositoryUrl="" \ + -PreleaseRepositoryUsername="$CENTRAL_PORTAL_USERNAME" \ + -PreleaseRepositoryPassword="$CENTRAL_PORTAL_PASSWORD" \ + publishMavenJavaPublicationToReleaseRepository --console=plain ``` ## Useful Checks ```bash -./gradlew -b build-export.gradle tasks --console=plain -./gradlew -b build-export.gradle publishToMavenLocal --info +./gradlew tasks --console=plain +./gradlew publishToMavenLocal --info ``` Release checklist for 0.8.0 and later: @@ -116,6 +123,6 @@ Release checklist for 0.8.0 and later: 1. Run `./gradlew clean test` on JDK 8. 2. Run `./gradlew clean test` on JDK 11. 3. Run `source scripts/load-maven-env.sh && ./gradlew cleanTest test` on JDK 21. -4. Run `source scripts/load-maven-env.sh && ./gradlew -b build-export.gradle -PcentralUsername="$CENTRAL_PORTAL_USERNAME" -PcentralPassword="$CENTRAL_PORTAL_PASSWORD" publishToMavenLocal` on JDK 21. +4. Run `source scripts/load-maven-env.sh && ./gradlew publishToMavenLocal` on JDK 21. 5. Run `source scripts/load-maven-env.sh && scripts/release-preflight.sh`. 6. Publish only after the JDK 8, 11, and 21 validation steps all pass. \ No newline at end of file diff --git a/README.md b/README.md index d523bcd..f5c69f6 100755 --- a/README.md +++ b/README.md @@ -6,20 +6,20 @@ Java library and CLI to generate Internet Computer artifacts. To add IC4J Codegen to your Java project, use Maven Central. - -https://search.maven.org/artifact/org.ic4j/ic4j-codegen/0.8.0/jar + +https://search.maven.org/artifact/org.ic4j/ic4j-codegen/0.8.5/jar ```xml org.ic4j ic4j-codegen - 0.8.0 + 0.8.5 ``` ```gradle -implementation 'org.ic4j:ic4j-codegen:0.8.0' +implementation 'org.ic4j:ic4j-codegen:0.8.5' ``` ## Build diff --git a/build.gradle b/build.gradle index ae79184..0006dbb 100755 --- a/build.gradle +++ b/build.gradle @@ -7,17 +7,25 @@ */ plugins { - id 'java' + id 'java-library' id 'eclipse' + id 'maven-publish' + id 'signing' } + group 'org.ic4j' -version '0.8.0' +version '0.8.5' sourceCompatibility = 1.8 targetCompatibility = 1.8 +java { + withSourcesJar() + withJavadocJar() +} + tasks.withType(JavaCompile).configureEach { if (JavaVersion.current().isJava9Compatible()) { options.release = 8 @@ -48,8 +56,10 @@ task fatJar(type: Jar) { } archiveBaseName = 'ic4j-codegen-with-dependencies' - - from { configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) } } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA' + from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } with jar } @@ -64,10 +74,10 @@ dependencies { compileOnly name: 'android-31' compileOnly name: 'react-android-0.71.11' - implementation group: 'org.ic4j', name: 'ic4j-reactnative', version: '0.8.0' - implementation group: 'org.ic4j', name: 'ic4j-agent', version: '0.8.0' - implementation group: 'org.ic4j', name: 'ic4j-candid', version: '0.8.1' - implementation group: 'org.ic4j', name: 'ic4j-spring', version: '0.8.0' + api group: 'org.ic4j', name: 'ic4j-reactnative', version: '0.8.0' + api group: 'org.ic4j', name: 'ic4j-agent', version: '0.8.5' + api group: 'org.ic4j', name: 'ic4j-candid', version: '0.8.5' + api group: 'org.ic4j', name: 'ic4j-spring', version: '0.8.0' // https://mvnrepository.com/artifact/commons-codec/commons-codec implementation group: 'commons-codec', name: 'commons-codec', version: '1.21.0' @@ -76,22 +86,23 @@ dependencies { implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.20.0' // https://mvnrepository.com/artifact/org.springframework/spring-context - implementation group: 'org.springframework', name: 'spring-context', version: '5.3.39' + api group: 'org.springframework', name: 'spring-context', version: '5.3.39' - // https://mvnrepository.com/artifact/jakarta.annotation/jakarta.annotation-api - implementation group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '3.0.0' + // Spring 5 generated services use javax.annotation.PostConstruct. + api group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2' // JAXB dependencies implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1' // https://mvnrepository.com/artifact/com.squareup/javapoet - implementation group: 'com.squareup', name: 'javapoet', version: '1.13.0' + api group: 'com.squareup', name: 'javapoet', version: '1.13.0' // https://mvnrepository.com/artifact/org.apache.ws.xmlschema/xmlschema-core implementation group: 'org.apache.ws.xmlschema', name: 'xmlschema-core', version: '2.3.2' // https://mvnrepository.com/artifact/org.slf4j/slf4j-simple - implementation group: 'org.slf4j', name: 'slf4j-simple', version: '2.0.17' + implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.17' + runtimeOnly group: 'org.slf4j', name: 'slf4j-simple', version: '2.0.17' // https://mvnrepository.com/artifact/info.picocli/picocli implementation group: 'info.picocli', name: 'picocli', version: '4.7.7' @@ -117,3 +128,80 @@ dependencies { } +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + pom { + name = 'IC4J Codegen' + description = 'Java and React Native code generators for Internet Computer Candid services.' + url = 'https://github.com/ic4j/ic4j-codegen' + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'ic4j' + name = 'IC4J contributors' + } + } + scm { + connection = 'scm:git:https://github.com/ic4j/ic4j-codegen.git' + developerConnection = 'scm:git:ssh://git@github.com/ic4j/ic4j-codegen.git' + url = 'https://github.com/ic4j/ic4j-codegen' + } + } + } + } + repositories { + if (providers.gradleProperty('releaseRepositoryUrl').isPresent()) { + maven { + name = 'release' + url = uri(providers.gradleProperty('releaseRepositoryUrl').get()) + credentials { + username = providers.gradleProperty('releaseRepositoryUsername') + .orElse(providers.environmentVariable('CENTRAL_PORTAL_USERNAME')).getOrElse('') + password = providers.gradleProperty('releaseRepositoryPassword') + .orElse(providers.environmentVariable('CENTRAL_PORTAL_PASSWORD')).getOrElse('') + } + } + } + } +} + +signing { + def signingKey = providers.environmentVariable('SIGNING_KEY').orNull + def signingPassword = providers.environmentVariable('SIGNING_PASSWORD').orNull + if (signingKey) + useInMemoryPgpKeys(signingKey, signingPassword) + sign publishing.publications.mavenJava + required { + gradle.taskGraph.allTasks.any { + it.name.startsWith('publish') && it.name.endsWith('Repository') && !it.name.contains('MavenLocal') + } + } +} + +tasks.withType(Sign).configureEach { + onlyIf { + gradle.taskGraph.allTasks.any { + it.name.startsWith('publish') && it.name.endsWith('Repository') && !it.name.contains('MavenLocal') + } + } +} + +tasks.register('compileGeneratedTestSources', JavaCompile) { + dependsOn test + source fileTree('build/generated/test-sources') { + include '**/*.java' + } + classpath = sourceSets.test.runtimeClasspath + destinationDirectory = layout.buildDirectory.dir('classes/java/generatedTest') + if (JavaVersion.current().isJava9Compatible()) + options.release = 8 +} + +check.dependsOn tasks.named('compileGeneratedTestSources') diff --git a/jbang/IC4JJBang.java b/jbang/IC4JJBang.java index b37ab61..462ee9b 100644 --- a/jbang/IC4JJBang.java +++ b/jbang/IC4JJBang.java @@ -8,14 +8,14 @@ //JAVA 11+ //REPOS mavencentral -//DEPS org.ic4j:ic4j-codegen:0.7.1 -//DEPS org.ic4j:ic4j-agent:0.7.1 -//DEPS org.ic4j:ic4j-reactnative:0.7.1 -//DEPS org.ic4j:ic4j-spring:0.7.1.1 -//DEPS org.ic4j:ic4j-candid:0.7.1 -//DEPS org.slf4j:slf4j-simple:2.0.13 +//DEPS org.ic4j:ic4j-codegen:0.8.5 +//DEPS org.ic4j:ic4j-agent:0.8.5 +//DEPS org.ic4j:ic4j-reactnative:0.8.0 +//DEPS org.ic4j:ic4j-spring:0.8.0 +//DEPS org.ic4j:ic4j-candid:0.8.5 +//DEPS org.slf4j:slf4j-simple:2.0.17 //DEPS com.squareup:javapoet:1.13.0 -//DEPS info.picocli:picocli:4.7.6 +//DEPS info.picocli:picocli:4.7.7 /** * Main to run IC4JJBang diff --git a/src/main/java/org/ic4j/codegen/CodegenException.java b/src/main/java/org/ic4j/codegen/CodegenException.java index e7a0060..0ec8b99 100644 --- a/src/main/java/org/ic4j/codegen/CodegenException.java +++ b/src/main/java/org/ic4j/codegen/CodegenException.java @@ -7,4 +7,8 @@ public CodegenException(String message) super(message); } + public CodegenException(String message, Throwable cause) + { + super(message, cause); + } } diff --git a/src/main/java/org/ic4j/codegen/IC4J.java b/src/main/java/org/ic4j/codegen/IC4J.java index 61b3588..7e967f1 100644 --- a/src/main/java/org/ic4j/codegen/IC4J.java +++ b/src/main/java/org/ic4j/codegen/IC4J.java @@ -26,7 +26,8 @@ import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; -@Command(name = "ic4j") +@Command(name = "ic4j", mixinStandardHelpOptions = true, + description = "Generates Internet Computer client artifacts from Candid IDL.") public class IC4J extends IC4JBase implements Callable { static final String DEFAULT_NETWORK = "http://localhost:4943/"; @@ -35,7 +36,7 @@ public class IC4J extends IC4JBase implements Callable { @Option(names = { "--identity" }) private String identityFile; - @Option(names = { "--identity-type" }) + @Option(names = { "--identity-type" }, description = "Identity type: basic, secp256k1, or prime256v1") String identityType; @Option(names = { "--network" }) @@ -51,7 +52,7 @@ public class IC4J extends IC4JBase implements Callable { private boolean annotate = false; @Option(names = { "--package-name" }) - private String packageName; + private String packageName = ""; @Option(names = { "--output-dir" }) private String outputDir = ""; @@ -65,11 +66,11 @@ public static void main(String[] args) { } public Integer call() throws Exception { - System.out.println("Subcommand needed: 'java', 'spring', 'reactnative', 'motoko' "); - return 0; + System.err.println("Subcommand needed: 'java', 'spring', or 'reactnative'"); + return CommandLine.ExitCode.USAGE; } - @Command(name = "java", description = "Generates Java Proxy inteface from IDL file or canister") + @Command(name = "java", description = "Generates Java proxy interface from IDL file or canister") public void createJavaProxy( @Parameters(index = "0", paramLabel = "", description = "Java Proxy interface name") String className) throws Exception { try { @@ -77,7 +78,6 @@ public void createJavaProxy( createJavaProxy(this.outputDir, this.packageName,className,this.verbose,this.annotate,this.candid,this.canisterId,this.network,this.identityFile, this.identityType); } catch (Exception e) { - LOG.info(e.getLocalizedMessage()); LOG.error(e.getLocalizedMessage(), e); throw e; @@ -94,7 +94,6 @@ public void createSpringService( createSpringService(this.outputDir, this.packageName, serviceClassName, className,this.verbose,this.annotate,this.candid,this.canisterId,this.network,this.identityFile, this.identityType); } catch (Exception e) { - LOG.info(e.getLocalizedMessage()); LOG.error(e.getLocalizedMessage(), e); throw e; @@ -110,7 +109,6 @@ public void createReactNativeModule( createReactNativeModule(this.outputDir, this.packageName,className,this.verbose,this.annotate,this.candid,this.canisterId,this.network,this.identityFile, this.identityType); } catch (Exception e) { - LOG.info(e.getLocalizedMessage()); LOG.error(e.getLocalizedMessage(), e); throw e; diff --git a/src/main/java/org/ic4j/codegen/IC4JBase.java b/src/main/java/org/ic4j/codegen/IC4JBase.java index 0494d48..8bba40a 100644 --- a/src/main/java/org/ic4j/codegen/IC4JBase.java +++ b/src/main/java/org/ic4j/codegen/IC4JBase.java @@ -1,6 +1,5 @@ package org.ic4j.codegen; -import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.io.StringReader; @@ -16,6 +15,7 @@ import org.ic4j.agent.identity.AnonymousIdentity; import org.ic4j.agent.identity.BasicIdentity; import org.ic4j.agent.identity.Identity; +import org.ic4j.agent.identity.Prime256v1Identity; import org.ic4j.agent.identity.Secp256k1Identity; import org.ic4j.candid.parser.IDLParser; import org.ic4j.candid.parser.IDLType; @@ -24,49 +24,72 @@ public abstract class IC4JBase { protected static IDLParser parseIDL(String candid, String canisterId, String network, String identityFile, String identityType) throws IOException, URISyntaxException, CodegenException { - Reader idlReader; + String normalizedIdentityType = normalizeIdentityType(identityType); if (candid != null) { - try { - idlReader = Files - .newBufferedReader(Paths.get(candid)); - } catch (Throwable t) { - throw new CodegenException("Cannot read IDL file " + candid); + try (Reader idlReader = Files.newBufferedReader(Paths.get(candid))) { + return parseIDL(idlReader); + } catch (IOException e) { + throw new CodegenException("Cannot read IDL file " + candid, e); } - } else { - if(canisterId == null) - throw new CodegenException("Define properly option --canister-id"); - - Principal canister = Principal.fromString(canisterId); - - Identity identity = new AnonymousIdentity(); + } - if (identityFile != null) { - Reader sourceReader = new FileReader(identityFile); + if(isBlank(canisterId)) + throw new CodegenException("Define option --canister-id when --candid is not provided"); + if(isBlank(network)) + throw new CodegenException("Define option --network when --candid is not provided"); - if (identityType != null && "basic".equals(identityType)) - identity = BasicIdentity.fromPEMFile(sourceReader); - else - identity = Secp256k1Identity.fromPEMFile(sourceReader); - } - - - if(canisterId == null) - throw new CodegenException("Define properly option --network"); - - ReplicaTransport transport = ReplicaOkHttpTransport.create(network); + Principal canister = Principal.fromString(canisterId); + Identity identity = loadIdentity(identityFile, normalizedIdentityType); + ReplicaTransport transport = ReplicaOkHttpTransport.create(network); + try { Agent agent = new AgentBuilder().transport(transport).identity(identity).build(); - String serviceIDL = agent.getIDL(canister); - - idlReader = new StringReader(serviceIDL); + return parseIDL(new StringReader(serviceIDL)); + } finally { + transport.close(); } + } + private static IDLParser parseIDL(Reader idlReader) throws IOException { IDLParser idlParser = new IDLParser(idlReader); - idlParser.parse(); - return idlParser; } + + private static Identity loadIdentity(String identityFile, String identityType) throws CodegenException { + if(isBlank(identityFile)) + return new AnonymousIdentity(); + + java.nio.file.Path identityPath = Paths.get(identityFile); + try { + switch(identityType) { + case "basic": + return BasicIdentity.fromPEMFile(identityPath); + case "prime256v1": + return Prime256v1Identity.fromPEMFile(identityPath); + case "secp256k1": + default: + return Secp256k1Identity.fromPEMFile(identityPath); + } + } catch (RuntimeException e) { + throw new CodegenException("Cannot load " + identityType + " identity from " + identityFile, e); + } + } + + static String normalizeIdentityType(String identityType) throws CodegenException { + if(isBlank(identityType)) + return "secp256k1"; + + String normalized = identityType.trim().toLowerCase(java.util.Locale.ROOT); + if(!normalized.equals("basic") && !normalized.equals("secp256k1") && !normalized.equals("prime256v1")) + throw new CodegenException("Unsupported identity type '" + identityType + + "'. Expected basic, secp256k1, or prime256v1"); + return normalized; + } + + private static boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } protected static void createJavaProxy(String outputDir, String packageName, String className, boolean verbose, boolean annotate, String candid, String canisterId, String network, String identityFile, String identityType) throws CodegenException, IOException, URISyntaxException { IDLParser idlParser = parseIDL(candid,canisterId,network,identityFile, identityType); @@ -82,6 +105,7 @@ protected static void createJavaProxy(String outputDir, String packageName, Stri javaWriterContext.packageName = packageName; javaWriterContext.canisterId = canisterId; javaWriterContext.network = network; + javaWriterContext.identityType = normalizeWriterIdentityType(identityType); javaWriterContext.verbose = verbose; javaWriterContext.annotate = annotate; @@ -102,6 +126,7 @@ protected static void createSpringService(String outputDir, String packageName, springWriterContext.packageName = packageName; springWriterContext.canisterId = canisterId; springWriterContext.network = network; + springWriterContext.identityType = normalizeWriterIdentityType(identityType); springWriterContext.verbose = verbose; springWriterContext.annotate = annotate; @@ -123,9 +148,14 @@ protected static void createReactNativeModule(String outputDir, String packageNa javaWriterContext.packageName = packageName; javaWriterContext.canisterId = canisterId; javaWriterContext.network = network; + javaWriterContext.identityType = normalizeWriterIdentityType(identityType); javaWriterContext.verbose = verbose; javaWriterContext.annotate = annotate; reactNativeWriter.write(javaWriterContext, Paths.get(outputDir), className, types, services); } + + private static String normalizeWriterIdentityType(String identityType) throws CodegenException { + return isBlank(identityType) ? null : normalizeIdentityType(identityType); + } } diff --git a/src/main/java/org/ic4j/codegen/JAXBGenerator.java b/src/main/java/org/ic4j/codegen/JAXBGenerator.java index 2bf8d60..b917b0a 100755 --- a/src/main/java/org/ic4j/codegen/JAXBGenerator.java +++ b/src/main/java/org/ic4j/codegen/JAXBGenerator.java @@ -57,6 +57,7 @@ import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; +import javax.xml.XMLConstants; public abstract class JAXBGenerator { @@ -69,9 +70,10 @@ public JAXBGenerator(TypeWriter typeWriter) { this.typeWriter = typeWriter; } - public void writeTypes(String dictionaryFileName, String outDir) + public void writeTypes(String dictionaryFileName, String outDir) throws CodegenException { Document xmlDocument; + int failures = 0; try { xmlDocument = getDocument(dictionaryFileName); final long timeoutMs = Long.getLong("ic4j.codegen.writeTypeTimeoutMs", 0L); @@ -113,22 +115,32 @@ public void writeTypes(String dictionaryFileName, String outDir) long elapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); LOG.info(String.format("[%d/%d] Generated %s in %d ms", i + 1, messageElements.getLength(), typeFileName, elapsed)); } catch (StackOverflowError e) { - LOG.error(String.format("Skipped recursive type graph for %s while generating %s", className, typeFileName)); + failures++; + LOG.error(String.format("Skipped recursive type graph for %s while generating %s", className, typeFileName), e); } catch (ClassNotFoundException e) { + failures++; LOG.error(String.format("Failed to load type class %s for %s", className, typeFileName), e); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - LOG.error(String.format("Timed out or interrupted while generating %s into %s", className, typeFileName), e); - } catch (RuntimeException e) { + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new CodegenException("Interrupted while generating " + typeFileName, e); + } catch (ExecutionException | TimeoutException e) { + failures++; + LOG.error(String.format("Failed or timed out while generating %s into %s", className, typeFileName), e); + } catch (IOException | RuntimeException e) { + failures++; LOG.error(String.format("Failed to generate type for %s into %s", className, typeFileName), e); } } } catch (SAXException | IOException | ParserConfigurationException | XPathExpressionException e) { - LOG.error(e.getLocalizedMessage(), e); - } + throw new CodegenException("Cannot generate types from dictionary " + dictionaryFileName, e); + } + + if(failures > 0) + throw new CodegenException("Failed to generate " + failures + " type file(s)"); } - public abstract void writeType(Class type, String outDir, String fileName); + public abstract void writeType(Class type, String outDir, String fileName) throws IOException; void writeTypeWithTimeout(final Class type, final String outDir, final String fileName, final long timeoutMs) throws InterruptedException, ExecutionException, TimeoutException @@ -146,7 +158,12 @@ void writeTypeWithTimeout(final Class type, final String outDir, final String return null; }; Future future = executor.submit(task); - future.get(timeoutMs, TimeUnit.MILLISECONDS); + try { + future.get(timeoutMs, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + future.cancel(true); + throw e; + } } finally { executor.shutdownNow(); } @@ -171,12 +188,18 @@ Set getSkippedClasses() static Document getDocument(String fileName) throws SAXException, IOException, ParserConfigurationException { - FileInputStream fileInputStream = new FileInputStream(fileName); DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); + builderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + builderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); + builderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + builderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + builderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + builderFactory.setXIncludeAware(false); + builderFactory.setExpandEntityReferences(false); DocumentBuilder builder = builderFactory.newDocumentBuilder(); - Document xmlDocument = builder.parse(fileInputStream); - - return xmlDocument; + try (FileInputStream fileInputStream = new FileInputStream(fileName)) { + return builder.parse(fileInputStream); + } } void logTypeGraph(Class rootType, int maxDepth) { diff --git a/src/main/java/org/ic4j/codegen/JakartaJAXBGenerator.java b/src/main/java/org/ic4j/codegen/JakartaJAXBGenerator.java index e356226..08dbaa9 100644 --- a/src/main/java/org/ic4j/codegen/JakartaJAXBGenerator.java +++ b/src/main/java/org/ic4j/codegen/JakartaJAXBGenerator.java @@ -30,6 +30,10 @@ public class JakartaJAXBGenerator extends JAXBGenerator { public static void main(String[] args) { + if(args.length < 2) { + System.err.println("Usage: JakartaJAXBGenerator [convert-name]"); + System.exit(2); + } MotokoWriter motokoWriter = new MotokoWriter(); @@ -44,7 +48,12 @@ public static void main(String[] args) JAXBGenerator jaxbGenerator = new JakartaJAXBGenerator(motokoWriter ); - jaxbGenerator.writeTypes(args[0], args[1]); + try { + jaxbGenerator.writeTypes(args[0], args[1]); + } catch (CodegenException e) { + LOG.error(e.getMessage(), e); + System.exit(1); + } } public JakartaJAXBGenerator(TypeWriter typeWriter) { @@ -53,19 +62,12 @@ public JakartaJAXBGenerator(TypeWriter typeWriter) { @Override - public void writeType(Class type, String outDir, String fileName) + public void writeType(Class type, String outDir, String fileName) throws IOException { - - IDLType idlType = JAXBUtils.getIDLType(type); - try { - if(Files.notExists(Paths.get(outDir))) - Files.createDirectories(Paths.get(outDir)); - - typeWriter.writeFile(Paths.get(outDir,fileName), idlType); - } catch (IOException e) { - LOG.error(e.getLocalizedMessage(), e); - } + if(Files.notExists(Paths.get(outDir))) + Files.createDirectories(Paths.get(outDir)); + typeWriter.writeFile(Paths.get(outDir,fileName), idlType); } diff --git a/src/main/java/org/ic4j/codegen/JavaIdentifier.java b/src/main/java/org/ic4j/codegen/JavaIdentifier.java new file mode 100644 index 0000000..4fe2e84 --- /dev/null +++ b/src/main/java/org/ic4j/codegen/JavaIdentifier.java @@ -0,0 +1,74 @@ +/* + * Copyright 2026 Exilor Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +package org.ic4j.codegen; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +final class JavaIdentifier { + private static final Set KEYWORDS = new HashSet<>(Arrays.asList( + "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", + "const", "continue", "default", "do", "double", "else", "enum", "extends", "final", + "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", + "interface", "long", "native", "new", "package", "private", "protected", "public", + "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", + "throw", "throws", "transient", "try", "void", "volatile", "while", "true", "false", + "null", "_")); + + private JavaIdentifier() { + } + + static String className(String value) { + return normalize(value, true, "GeneratedType"); + } + + static String memberName(String value) { + return normalize(value, false, "generatedMember"); + } + + static String unique(String candidate, Set used) { + String unique = candidate; + int suffix = 2; + while (!used.add(unique)) + unique = candidate + suffix++; + return unique; + } + + private static String normalize(String value, boolean capitalize, String fallback) { + if (value == null || value.isEmpty()) + return fallback; + + StringBuilder result = new StringBuilder(); + boolean capitalizeNext = capitalize; + for (int i = 0; i < value.length(); i++) { + char current = value.charAt(i); + if (!Character.isJavaIdentifierPart(current)) { + capitalizeNext = result.length() > 0; + continue; + } + + if (result.length() == 0 && !Character.isJavaIdentifierStart(current)) + result.append(capitalize ? "Type" : "value"); + + result.append(capitalizeNext ? Character.toUpperCase(current) : current); + capitalizeNext = false; + } + + if (result.length() == 0) + result.append(fallback); + if (!capitalize) + result.setCharAt(0, Character.toLowerCase(result.charAt(0))); + if (KEYWORDS.contains(result.toString())) + result.append(capitalize ? "Type" : "Value"); + return result.toString(); + } +} diff --git a/src/main/java/org/ic4j/codegen/JavaWriter.java b/src/main/java/org/ic4j/codegen/JavaWriter.java index dd54648..f895854 100755 --- a/src/main/java/org/ic4j/codegen/JavaWriter.java +++ b/src/main/java/org/ic4j/codegen/JavaWriter.java @@ -25,6 +25,7 @@ import java.math.BigInteger; import java.nio.file.Path; import java.util.Map; +import java.util.HashSet; import java.util.Set; import org.ic4j.agent.annotations.Agent; @@ -66,16 +67,14 @@ public void write(JavaWriterContext javaWriterContext, Path path, String proxyNa { javaWriterContext.writer = new StringWriter(); - JavaWriter javaWriter = new JavaWriter(); - for(IDLType idlType : types.values()) { - javaWriter.generateType(javaWriterContext, idlType); + this.generateType(javaWriterContext, idlType); } for(IDLType idlType : services.values()) { - javaWriter.generateProxy(javaWriterContext,proxyName, idlType); + this.generateProxy(javaWriterContext,proxyName, idlType); } Set keys = javaWriterContext.types.keySet(); @@ -118,10 +117,11 @@ public void generateProxy(JavaWriterContext context,String proxyName, IDLType id IdentityType identityType = IdentityType.ANONYMOUS; if(context.identityType != null) { - switch(context.identityType) + switch(context.identityType.toLowerCase(java.util.Locale.ROOT)) { - case "Basic" : identityType = IdentityType.BASIC;break; - case "Secp256k1" : identityType = IdentityType.SECP256K1;break; + case "basic" : identityType = IdentityType.BASIC;break; + case "secp256k1" : identityType = IdentityType.SECP256K1;break; + case "prime256v1" : identityType = IdentityType.PRIME256V1;break; } } @@ -141,6 +141,7 @@ public void generateProxy(JavaWriterContext context,String proxyName, IDLType id Map meths = idlType.getMeths(); Set names = meths.keySet(); + Set methodNames = new HashSet<>(); for(String name : names) { @@ -150,10 +151,7 @@ public void generateProxy(JavaWriterContext context,String proxyName, IDLType id String funcName = name; - funcName = this.normalizeMethodName(funcName); - - if(name.equals("void")) - funcName = "voidFunc"; + funcName = JavaIdentifier.unique(this.normalizeMethodName(funcName), methodNames); MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(funcName) .addAnnotation(AnnotationSpec.builder(Name.class).addMember("value", "$S", name).build()) @@ -195,6 +193,8 @@ else if(methType.modes.get(0) == Mode.ONEWAY) } } + if(methType.rets.size() > 1) + throw new IOException("Java generation does not support multiple return values for method " + name); if(!methType.rets.isEmpty()) { IDLType retType = methType.rets.get(0); @@ -243,7 +243,8 @@ public void generateType(JavaWriterContext context, IDLType idlType) throws IOEx } if(idlType.getType() == Type.RECORD) - { + { + name = context.claimTypeName(idlType, name); TypeSpec recordSpec = this.generateRecord(context, name, idlType); JavaFile javaFile = JavaFile.builder(context.packageName, recordSpec) @@ -254,7 +255,8 @@ public void generateType(JavaWriterContext context, IDLType idlType) throws IOEx return; } if(idlType.getType() == Type.VARIANT) - { + { + name = context.claimTypeName(idlType, name); TypeSpec variantSpec = this.generateVariant(context, name, idlType); JavaFile javaFile = JavaFile.builder(context.packageName, variantSpec) @@ -275,6 +277,7 @@ public TypeSpec generateRecord(JavaWriterContext context,String typeName, IDLTyp Map types = idlType.getTypeMap(); Set