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..404200f 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` @@ -36,12 +36,12 @@ scripts/release-preflight.sh ## Dry Runs -Run dry runs first to validate task wiring and credentials without uploading artifacts: +Run a dry run first to validate local publication task wiring without uploading artifacts. +Use `scripts/release-preflight.sh` above to validate required release credentials: ```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 +80,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 +94,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 +124,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..c85703a 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 @@ -41,6 +49,14 @@ jar { archiveBaseName = 'ic4j-codegen' } +configurations { + fatJarCompileOnly { + canBeConsumed = false + canBeResolved = true + extendsFrom compileOnly + } +} + task fatJar(type: Jar) { manifest { attributes('Implementation-Title': 'ic4j-codegen', @@ -48,8 +64,13 @@ 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 + configurations.fatJarCompileOnly) + .collect { it.isDirectory() ? it : zipTree(it) } + } with jar } @@ -64,10 +85,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 +97,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 +139,86 @@ 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') + } + } +} + +def generatedTestSourcesDir = layout.buildDirectory.dir('generated/test-sources') + +test { + outputs.dir generatedTestSourcesDir +} + +tasks.register('compileGeneratedTestSources', JavaCompile) { + dependsOn test + source fileTree(generatedTestSourcesDir) { + 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/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..eddabd2 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ 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..5cac62d 100755 --- a/src/main/java/org/ic4j/codegen/JAXBGenerator.java +++ b/src/main/java/org/ic4j/codegen/JAXBGenerator.java @@ -28,14 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.lang.reflect.Field; @@ -57,6 +50,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,12 +63,12 @@ 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); final Set skippedClasses = this.getSkippedClasses(); final boolean traceGraph = Boolean.getBoolean("ic4j.codegen.traceTypeGraph"); final int traceDepth = Integer.getInteger("ic4j.codegen.traceTypeGraphDepth", 4); @@ -106,51 +100,30 @@ public void writeTypes(String dictionaryFileName, String outDir) Class typeClass = Class.forName(className); if(traceGraph) this.logTypeGraph(typeClass, traceDepth); - if(timeoutMs > 0) - this.writeTypeWithTimeout(typeClass, typeOutDir, typeFileName, timeoutMs); - else - this.writeType(typeClass, typeOutDir, typeFileName); + this.writeType(typeClass, typeOutDir, typeFileName); 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 (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); - } - } - - public abstract void writeType(Class type, String outDir, String fileName); - - void writeTypeWithTimeout(final Class type, final String outDir, final String fileName, final long timeoutMs) - throws InterruptedException, ExecutionException, TimeoutException - { - ThreadFactory threadFactory = runnable -> { - Thread thread = new Thread(runnable, "jaxb-write-type"); - thread.setDaemon(true); - return thread; - }; - - ExecutorService executor = Executors.newSingleThreadExecutor(threadFactory); - try { - Callable task = () -> { - this.writeType(type, outDir, fileName); - return null; - }; - Future future = executor.submit(task); - future.get(timeoutMs, TimeUnit.MILLISECONDS); - } finally { - executor.shutdownNow(); + 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) throws IOException; Set getSkippedClasses() { @@ -171,12 +144,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..2e65469 --- /dev/null +++ b/src/main/java/org/ic4j/codegen/JavaIdentifier.java @@ -0,0 +1,83 @@ +/* + * 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 static final Set RESERVED_METHOD_NAMES = new HashSet<>( + Arrays.asList("getClass", "notify", "notifyAll", "wait")); + + private JavaIdentifier() { + } + + static String className(String value) { + return normalize(value, true, "GeneratedType"); + } + + static String memberName(String value) { + return normalize(value, false, "generatedMember"); + } + + static String methodName(String value) { + String methodName = normalize(value, false, "generatedMethod"); + if (RESERVED_METHOD_NAMES.contains(methodName)) + methodName += "Value"; + return methodName; + } + + 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..6551b10 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