UNPKG

jsii-pacmak

Version:

A code generation framework for jsii backend languages

1,135 lines (1,133 loc) 117 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JavaBuilder = void 0; const spec = require("@jsii/spec"); const assert = require("assert"); const clone = require("clone"); const case_utils_1 = require("codemaker/lib/case-utils"); const crypto_1 = require("crypto"); const fs = require("fs-extra"); const jsii_rosetta_1 = require("jsii-rosetta"); const path = require("path"); const xmlbuilder = require("xmlbuilder"); const generator_1 = require("../generator"); const logging = require("../logging"); const naming_util_1 = require("../naming-util"); const target_1 = require("../target"); const util_1 = require("../util"); const version_1 = require("../version"); const _utils_1 = require("./_utils"); const version_utils_1 = require("./version-utils"); const index_1 = require("./index"); // eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports const spdxLicenseList = require('spdx-license-list'); const BUILDER_CLASS_NAME = 'Builder'; const ANN_NOT_NULL = '@org.jetbrains.annotations.NotNull'; const ANN_NULLABLE = '@org.jetbrains.annotations.Nullable'; const ANN_INTERNAL = '@software.amazon.jsii.Internal'; /** * Build Java packages all together, by generating an aggregate POM * * This will make the Java build a lot more efficient (~300%). * * Do this by copying the code into a temporary directory, generating an aggregate * POM there, and then copying the artifacts back into the respective output * directories. */ class JavaBuilder { constructor(modules, options) { this.modules = modules; this.options = options; this.targetName = 'java'; } async buildModules() { if (this.modules.length === 0) { return; } if (this.options.codeOnly) { // Simple, just generate code to respective output dirs await Promise.all(this.modules.map((module) => this.generateModuleCode(module, this.options, this.outputDir(module.outputDirectory)))); return; } // Otherwise make a single tempdir to hold all sources, build them together and copy them back out const scratchDirs = []; try { const tempSourceDir = await this.generateAggregateSourceDir(this.modules, this.options); scratchDirs.push(tempSourceDir); // Need any old module object to make a target to be able to invoke build, though none of its settings // will be used. const target = this.makeTarget(this.modules[0], this.options); const tempOutputDir = await util_1.Scratch.make(async (dir) => { logging.debug(`Building Java code to ${dir}`); await target.build(tempSourceDir.directory, dir); }); scratchDirs.push(tempOutputDir); await this.copyOutArtifacts(tempOutputDir.directory, tempSourceDir.object); if (this.options.clean) { await util_1.Scratch.cleanupAll(scratchDirs); } } catch (e) { logging.warn(`Exception occurred, not cleaning up ${scratchDirs .map((s) => s.directory) .join(', ')}`); throw e; } } async generateModuleCode(module, options, where) { const target = this.makeTarget(module, options); logging.debug(`Generating Java code into ${where}`); await target.generateCode(where, module.tarball); } async generateAggregateSourceDir(modules, options) { return util_1.Scratch.make(async (tmpDir) => { logging.debug(`Generating aggregate Java source dir at ${tmpDir}`); const ret = []; const generatedModules = modules .map((module) => ({ module, relativeName: (0, util_1.slugify)(module.name) })) .map(({ module, relativeName }) => ({ module, relativeName, sourceDir: path.join(tmpDir, relativeName), })) .map(({ module, relativeName, sourceDir }) => this.generateModuleCode(module, options, sourceDir).then(() => ({ module, relativeName, }))); for await (const { module, relativeName } of generatedModules) { ret.push({ relativeSourceDir: relativeName, relativeArtifactsDir: moduleArtifactsSubdir(module), outputTargetDirectory: module.outputDirectory, }); } await this.generateAggregatePom(tmpDir, ret.map((m) => m.relativeSourceDir)); await this.generateMavenSettingsForLocalDeps(tmpDir); return ret; }); } async generateAggregatePom(where, moduleNames) { const aggregatePom = xmlbuilder .create({ 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 https://maven.apache.org/xsd/maven-4.0.0.xsd', '#comment': [ `Generated by jsii-pacmak@${version_1.VERSION_DESC} on ${new Date().toISOString()}`, ], modelVersion: '4.0.0', packaging: 'pom', groupId: 'software.amazon.jsii', artifactId: 'aggregatepom', version: '1.0.0', modules: { module: moduleNames, }, }, }, { encoding: 'UTF-8' }) .end({ pretty: true }); logging.debug(`Generated ${where}/pom.xml`); await fs.writeFile(path.join(where, 'pom.xml'), aggregatePom); } async copyOutArtifacts(artifactsRoot, packages) { logging.debug('Copying out Java artifacts'); // The artifacts directory looks like this: // /tmp/XXX/software/amazon/awscdk/something/v1.2.3 // /else/v1.2.3 // /entirely/v1.2.3 // // We get the 'software/amazon/awscdk/something' path from the package, identifying // the files we need to copy, including Maven metadata. But we need to recreate // the whole path in the target directory. await Promise.all(packages.map(async (pkg) => { const artifactsSource = path.join(artifactsRoot, pkg.relativeArtifactsDir); const artifactsDest = path.join(this.outputDir(pkg.outputTargetDirectory), pkg.relativeArtifactsDir); await fs.mkdirp(artifactsDest); await fs.copy(artifactsSource, artifactsDest, { recursive: true }); })); } /** * Decide whether or not to append 'java' to the given output directory */ outputDir(declaredDir) { return this.options.languageSubdirectory ? path.join(declaredDir, this.targetName) : declaredDir; } /** * Generates maven settings file for this build. * @param where The generated sources directory. This is where user.xml will be placed. * @param currentOutputDirectory The current output directory. Will be added as a local maven repo. */ async generateMavenSettingsForLocalDeps(where) { const filePath = path.join(where, 'user.xml'); // traverse the dep graph of this module and find all modules that have // an <outdir>/java directory. we will add those as local maven // repositories which will resolve instead of Maven Central for those // module. this enables building against local modules (i.e. in lerna // repositories or linked modules). const allDepsOutputDirs = new Set(); const resolvedModules = this.modules.map(async (mod) => ({ module: mod, localBuildDirs: await (0, target_1.findLocalBuildDirs)(mod.moduleDirectory, this.targetName), })); for await (const { module, localBuildDirs } of resolvedModules) { (0, util_1.setExtend)(allDepsOutputDirs, localBuildDirs); // Also include output directory where we're building to, in case we build multiple packages into // the same output directory. allDepsOutputDirs.add(path.join(module.outputDirectory, this.options.languageSubdirectory ? this.targetName : '')); } const localRepos = Array.from(allDepsOutputDirs); // if java-runtime is checked-out and we can find a local repository, // add it to the list. const localJavaRuntime = await findJavaRuntimeLocalRepository(); if (localJavaRuntime) { localRepos.push(localJavaRuntime); } logging.debug('local maven repos:', localRepos); const profileName = 'local-jsii-modules'; const localRepository = this.options.arguments['maven-local-repository']; const settings = xmlbuilder .create({ settings: { '@xmlns': 'http://maven.apache.org/POM/4.0.0', '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', '@xsi:schemaLocation': 'http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd', '#comment': [ `Generated by jsii-pacmak@${version_1.VERSION_DESC} on ${new Date().toISOString()}`, ], // Do *not* attempt to ask the user for stuff... interactiveMode: false, // Use a non-default local repository (unless java-custom-cache-path arg is provided) to isolate from cached artifacts... localRepository: localRepository ? path.resolve(process.cwd(), localRepository) : path.resolve(where, '.m2', 'repository'), // Register locations of locally-sourced dependencies profiles: { profile: { id: profileName, repositories: { repository: localRepos.map((repo) => ({ id: repo.replace(/[\\/:"<>|?*]/g, '$'), url: `file://${repo}`, })), }, }, }, activeProfiles: { activeProfile: profileName, }, }, }, { encoding: 'UTF-8' }) .end({ pretty: true }); logging.debug(`Generated ${filePath}`); await fs.writeFile(filePath, settings); return filePath; } makeTarget(module, options) { return new Java({ arguments: options.arguments, assembly: module.assembly, fingerprint: options.fingerprint, force: options.force, packageDir: module.moduleDirectory, rosetta: options.rosetta, runtimeTypeChecking: options.runtimeTypeChecking, targetName: this.targetName, }); } } exports.JavaBuilder = JavaBuilder; /** * Return the subdirectory of the output directory where the artifacts for this particular package are produced */ function moduleArtifactsSubdir(module) { const groupId = module.assembly.targets.java.maven.groupId; const artifactId = module.assembly.targets.java.maven.artifactId; return `${groupId.replace(/\./g, '/')}/${artifactId}`; } class Java extends target_1.Target { constructor(options) { super(options); this.generator = new JavaGenerator(options); } static toPackageInfos(assm) { const groupId = assm.targets.java.maven.groupId; const artifactId = assm.targets.java.maven.artifactId; const releaseVersion = (0, version_utils_1.toReleaseVersion)(assm.version, index_1.TargetName.JAVA); const url = `https://repo1.maven.org/maven2/${groupId.replace(/\./g, '/')}/${artifactId}/${assm.version}/`; return { java: { repository: 'Maven Central', url, usage: { 'Apache Maven': { language: 'xml', code: xmlbuilder .create({ dependency: { groupId, artifactId, version: releaseVersion }, }) .end({ pretty: true }) .replace(/<\?\s*xml(\s[^>]+)?>\s*/m, ''), }, 'Apache Buildr': `'${groupId}:${artifactId}:jar:${releaseVersion}'`, 'Apache Ivy': { language: 'xml', code: xmlbuilder .create({ dependency: { '@groupId': groupId, '@name': artifactId, '@rev': releaseVersion, }, }) .end({ pretty: true }) .replace(/<\?\s*xml(\s[^>]+)?>\s*/m, ''), }, 'Groovy Grape': `@Grapes(\n@Grab(group='${groupId}', module='${artifactId}', version='${releaseVersion}')\n)`, 'Gradle / Grails': `compile '${groupId}:${artifactId}:${releaseVersion}'`, }, }, }; } static toNativeReference(type, options) { const [, ...name] = type.fqn.split('.'); return { java: `import ${[options.package, ...name].join('.')};` }; } async build(sourceDir, outDir) { const url = `file://${outDir}`; const mvnArguments = new Array(); for (const arg of Object.keys(this.arguments)) { if (!arg.startsWith('mvn-')) { continue; } mvnArguments.push(`--${arg.slice(4)}`); mvnArguments.push(this.arguments[arg].toString()); } await (0, util_1.shell)('mvn', [ // If we don't run in verbose mode, turn on quiet mode ...(this.arguments.verbose ? [] : ['--quiet']), '--batch-mode', ...mvnArguments, 'deploy', `-D=altDeploymentRepository=local::default::${url}`, '--settings=user.xml', ], { cwd: sourceDir, env: { // Twiddle the JVM settings a little for Maven. Delaying JIT compilation // brings down Maven execution time by about 1/3rd (15->10s, 30->20s) MAVEN_OPTS: `${process.env.MAVEN_OPTS ?? ''} -XX:+TieredCompilation -XX:TieredStopAtLevel=1`, }, retry: { maxAttempts: 5 }, }); } } exports.default = Java; // ################## // # CODE GENERATOR # // ################## const MODULE_CLASS_NAME = '$Module'; const INTERFACE_PROXY_CLASS_NAME = 'Jsii$Proxy'; const INTERFACE_DEFAULT_CLASS_NAME = 'Jsii$Default'; class JavaGenerator extends generator_1.Generator { constructor(options) { super({ ...options, generateOverloadsForMethodWithOptionals: true }); /** * A map of all the modules ever referenced during code generation. These include * direct dependencies but can potentially also include transitive dependencies, when, * for example, we need to refer to their types when flatting the class hierarchy for * interface proxies. */ this.referencedModules = {}; this.rosetta = options.rosetta; } /** * Turns a raw javascript property name (eg: 'default') into a safe Java property name (eg: 'defaultValue'). * @param propertyName the raw JSII property Name */ static safeJavaPropertyName(propertyName) { if (!propertyName) { return propertyName; } if (propertyName === '_') { // Slightly different pattern for this one return '__'; } if (JavaGenerator.RESERVED_KEYWORDS.includes(propertyName)) { return `${propertyName}Value`; } return propertyName; } /** * Turns a raw javascript method name (eg: 'import') into a safe Java method name (eg: 'doImport'). * @param methodName */ static safeJavaMethodName(methodName) { if (!methodName) { return methodName; } if (methodName === '_') { // Different pattern for this one. Also this should never happen, who names a function '_' ?? return 'doIt'; } if (JavaGenerator.RESERVED_KEYWORDS.includes(methodName)) { return `do${(0, naming_util_1.jsiiToPascalCase)(methodName)}`; } return methodName; } onBeginAssembly(assm, fingerprint) { this.emitFullGeneratorInfo = fingerprint; this.moduleClass = this.emitModuleFile(assm); this.emitAssemblyPackageInfo(assm); } onEndAssembly(assm, fingerprint) { this.emitMavenPom(assm, fingerprint); delete this.emitFullGeneratorInfo; } getAssemblyOutputDir(mod) { const dir = this.toNativeFqn(mod.name).replace(/\./g, '/'); return path.join('src', 'main', 'resources', dir); } onBeginClass(cls, abstract) { this.openFileIfNeeded(cls); this.addJavaDocs(cls, { api: 'type', fqn: cls.fqn }); const classBase = this.getClassBase(cls); const extendsExpression = classBase ? ` extends ${classBase}` : ''; let implementsExpr = ''; if (cls.interfaces?.length ?? 0 > 0) { implementsExpr = ` implements ${cls .interfaces.map((x) => this.toNativeFqn(x)) .join(', ')}`; } const nested = this.isNested(cls); const inner = nested ? ' static' : ''; const absPrefix = abstract ? ' abstract' : ''; if (!nested) { this.emitGeneratedAnnotation(); } this.emitStabilityAnnotations(cls); this.code.line(`@software.amazon.jsii.Jsii(module = ${this.moduleClass}.class, fqn = "${cls.fqn}")`); this.code.openBlock(`public${inner}${absPrefix} class ${cls.name}${extendsExpression}${implementsExpr}`); this.emitJsiiInitializers(cls); this.emitStaticInitializer(cls); } onEndClass(cls) { if (cls.abstract) { const type = this.reflectAssembly.findType(cls.fqn); this.emitProxy(type); } else { this.emitClassBuilder(cls); } this.code.closeBlock(); this.closeFileIfNeeded(cls); } onInitializer(cls, method) { this.code.line(); // If needed, patching up the documentation to point users at the builder pattern this.addJavaDocs(method, { api: 'initializer', fqn: cls.fqn }); this.emitStabilityAnnotations(method); // Abstract classes should have protected initializers const initializerAccessLevel = cls.abstract ? 'protected' : this.renderAccessLevel(method); this.code.openBlock(`${initializerAccessLevel} ${cls.name}(${this.renderMethodParameters(method)})`); this.code.line('super(software.amazon.jsii.JsiiObject.InitializationMode.JSII);'); this.emitUnionParameterValdation(method.parameters); this.code.line(`software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this${this.renderMethodCallArguments(method)});`); this.code.closeBlock(); } onInitializerOverload(cls, overload, _originalInitializer) { this.onInitializer(cls, overload); } onField(_cls, _prop, _union) { /* noop */ } onProperty(cls, prop) { this.emitProperty(cls, prop, cls); } onStaticProperty(cls, prop) { if (prop.const) { this.emitConstProperty(cls, prop); } else { this.emitProperty(cls, prop, cls); } } /** * Since we expand the union setters, we will use this event to only emit the getter which returns an Object. */ onUnionProperty(cls, prop, _union) { this.emitProperty(cls, prop, cls); } onMethod(cls, method) { this.emitMethod(cls, method); } onMethodOverload(cls, overload, _originalMethod) { this.onMethod(cls, overload); } onStaticMethod(cls, method) { this.emitMethod(cls, method); } onStaticMethodOverload(cls, overload, _originalMethod) { this.emitMethod(cls, overload); } onBeginEnum(enm) { this.openFileIfNeeded(enm); this.addJavaDocs(enm, { api: 'type', fqn: enm.fqn }); if (!this.isNested(enm)) { this.emitGeneratedAnnotation(); } this.emitStabilityAnnotations(enm); this.code.line(`@software.amazon.jsii.Jsii(module = ${this.moduleClass}.class, fqn = "${enm.fqn}")`); this.code.openBlock(`public enum ${enm.name}`); } onEndEnum(enm) { this.code.closeBlock(); this.closeFileIfNeeded(enm); } onEnumMember(parentType, member) { this.addJavaDocs(member, { api: 'member', fqn: parentType.fqn, memberName: member.name, }); this.emitStabilityAnnotations(member); this.code.line(`${member.name},`); } /** * Namespaces are handled implicitly by onBeginClass(). * * Only emit package-info in case this is a submodule */ onBeginNamespace(ns) { const submodule = this.assembly.submodules?.[ns]; if (submodule) { this.emitSubmodulePackageInfo(this.assembly, ns); } } onEndNamespace(_ns) { /* noop */ } onBeginInterface(ifc) { this.openFileIfNeeded(ifc); this.addJavaDocs(ifc, { api: 'type', fqn: ifc.fqn }); // all interfaces always extend JsiiInterface so we can identify that it is a jsii interface. const interfaces = ifc.interfaces ?? []; const bases = [ 'software.amazon.jsii.JsiiSerializable', ...interfaces.map((x) => this.toNativeFqn(x)), ].join(', '); const nested = this.isNested(ifc); const inner = nested ? ' static' : ''; if (!nested) { this.emitGeneratedAnnotation(); } this.code.line(`@software.amazon.jsii.Jsii(module = ${this.moduleClass}.class, fqn = "${ifc.fqn}")`); this.code.line(`@software.amazon.jsii.Jsii.Proxy(${ifc.name}.${INTERFACE_PROXY_CLASS_NAME}.class)`); this.emitStabilityAnnotations(ifc); this.code.openBlock(`public${inner} interface ${ifc.name} extends ${bases}`); } onEndInterface(ifc) { this.emitMultiplyInheritedOptionalProperties(ifc); if (ifc.datatype) { this.emitDataType(ifc); } else { const type = this.reflectAssembly.findType(ifc.fqn); this.emitProxy(type); // We don't emit Jsii$Default if the assembly opted out of it explicitly. // This is mostly to facilitate compatibility testing... if (hasDefaultInterfaces(this.reflectAssembly)) { this.emitDefaultImplementation(type); } } this.code.closeBlock(); this.closeFileIfNeeded(ifc); } onInterfaceMethod(ifc, method) { this.code.line(); const returnType = method.returns ? this.toDecoratedJavaType(method.returns) : 'void'; const methodName = JavaGenerator.safeJavaMethodName(method.name); this.addJavaDocs(method, { api: 'member', fqn: ifc.fqn, memberName: methodName, }); this.emitStabilityAnnotations(method); this.code.line(`${returnType} ${methodName}(${this.renderMethodParameters(method)});`); } onInterfaceMethodOverload(ifc, overload, _originalMethod) { this.onInterfaceMethod(ifc, overload); } onInterfaceProperty(ifc, prop) { const getterType = this.toDecoratedJavaType(prop); const propName = (0, naming_util_1.jsiiToPascalCase)(JavaGenerator.safeJavaPropertyName(prop.name)); // for unions we only generate overloads for setters, not getters. this.code.line(); this.addJavaDocs(prop, { api: 'member', fqn: ifc.fqn, memberName: prop.name, }); this.emitStabilityAnnotations(prop); if (prop.optional) { if (prop.overrides) { this.code.line('@Override'); } this.code.openBlock(`default ${getterType} get${propName}()`); this.code.line('return null;'); this.code.closeBlock(); } else { this.code.line(`${getterType} get${propName}();`); } if (!prop.immutable) { const setterTypes = this.toDecoratedJavaTypes(prop); for (const type of setterTypes) { this.code.line(); this.addJavaDocs(prop, { api: 'member', fqn: ifc.fqn, memberName: prop.name, }); if (prop.optional) { if (prop.overrides) { this.code.line('@Override'); } this.code.line('@software.amazon.jsii.Optional'); this.code.openBlock(`default void set${propName}(final ${type} value)`); this.code.line(`throw new UnsupportedOperationException("'void " + getClass().getCanonicalName() + "#set${propName}(${type})' is not implemented!");`); this.code.closeBlock(); } else { this.code.line(`void set${propName}(final ${type} value);`); } } } } /** * Emits a local default implementation for optional properties inherited from * multiple distinct parent types. This remvoes the default method dispatch * ambiguity that would otherwise exist. * * @param ifc the interface to be processed. * * @see https://github.com/aws/jsii/issues/2256 */ emitMultiplyInheritedOptionalProperties(ifc) { if (ifc.interfaces == null || ifc.interfaces.length <= 1) { // Nothing to do if we don't have parent interfaces, or if we have exactly one return; } const inheritedOptionalProps = ifc.interfaces .map(allOptionalProps.bind(this)) // Calculate how many direct parents brought a given optional property .reduce((histogram, entry) => { for (const [name, spec] of Object.entries(entry)) { histogram[name] = histogram[name] ?? { spec, count: 0 }; histogram[name].count += 1; } return histogram; }, {}); const localProps = new Set(ifc.properties?.map((prop) => prop.name) ?? []); for (const { spec, count } of Object.values(inheritedOptionalProps)) { if (count < 2 || localProps.has(spec.name)) { continue; } this.onInterfaceProperty(ifc, spec); } function allOptionalProps(fqn) { const type = this.findType(fqn); const result = {}; for (const prop of type.properties ?? []) { // Adding artifical "overrides" here for code-gen quality's sake. result[prop.name] = { ...prop, overrides: type.fqn }; } // Include optional properties of all super interfaces in the result for (const base of type.interfaces ?? []) { for (const [name, prop] of Object.entries(allOptionalProps.call(this, base))) { if (!(name in result)) { result[name] = prop; } } } return result; } } emitAssemblyPackageInfo(mod) { if (!mod.docs) { return; } const { packageName } = this.toNativeName(mod); const packageInfoFile = this.toJavaFilePath(mod, `${mod.name}.package-info`); this.code.openFile(packageInfoFile); this.code.line('/**'); if (mod.readme) { for (const line of myMarkDownToJavaDoc(this.convertSamplesInMarkdown(mod.readme.markdown, { api: 'moduleReadme', moduleFqn: mod.name, })).split('\n')) { this.code.line(` * ${line.replace(/\*\//g, '*{@literal /}')}`); } } if (mod.docs.deprecated) { this.code.line(' *'); // Javac won't allow @deprecated on packages, while @Deprecated is aaaabsolutely fine. Duh. this.code.line(` * Deprecated: ${mod.docs.deprecated}`); } this.code.line(' */'); this.emitStabilityAnnotations(mod); this.code.line(`package ${packageName};`); this.code.closeFile(packageInfoFile); } emitSubmodulePackageInfo(assembly, moduleFqn) { const mod = assembly.submodules?.[moduleFqn]; if (!mod?.readme?.markdown) { return; } const { packageName } = translateFqn(assembly, moduleFqn); const packageInfoFile = this.toJavaFilePath(assembly, `${moduleFqn}.package-info`); this.code.openFile(packageInfoFile); this.code.line('/**'); if (mod.readme) { for (const line of myMarkDownToJavaDoc(this.convertSamplesInMarkdown(mod.readme.markdown, { api: 'moduleReadme', moduleFqn, })).split('\n')) { this.code.line(` * ${line.replace(/\*\//g, '*{@literal /}')}`); } } this.code.line(' */'); this.code.line(`package ${packageName};`); this.code.closeFile(packageInfoFile); } emitMavenPom(assm, fingerprint) { if (!assm.targets?.java) { throw new Error(`Assembly ${assm.name} does not declare a java target`); } const comment = fingerprint ? { '#comment': [ `Generated by jsii-pacmak@${version_1.VERSION_DESC} on ${new Date().toISOString()}`, `@jsii-pacmak:meta@ ${JSON.stringify(this.metadata)}`, ], } : {}; this.code.openFile('pom.xml'); this.code.line(xmlbuilder .create({ 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', ...comment, modelVersion: '4.0.0', name: '${project.groupId}:${project.artifactId}', description: assm.description, url: assm.homepage, licenses: { license: getLicense(), }, developers: { developer: mavenDevelopers(), }, scm: { connection: `scm:${assm.repository.type}:${assm.repository.url}`, url: assm.repository.url, }, groupId: assm.targets.java.maven.groupId, artifactId: assm.targets.java.maven.artifactId, version: makeVersion(assm.version, assm.targets.java.maven.versionSuffix), packaging: 'jar', properties: { 'project.build.sourceEncoding': 'UTF-8' }, dependencies: { dependency: mavenDependencies.call(this) }, build: { plugins: { plugin: [ { groupId: 'org.apache.maven.plugins', artifactId: 'maven-compiler-plugin', version: '3.11.0', configuration: { source: '1.8', target: '1.8', fork: 'true', maxmem: '4096m', }, }, { groupId: 'org.apache.maven.plugins', artifactId: 'maven-jar-plugin', version: '3.3.0', configuration: { archive: { index: true, manifest: { addDefaultImplementationEntries: true, addDefaultSpecificationEntries: true, }, }, }, }, { groupId: 'org.apache.maven.plugins', artifactId: 'maven-source-plugin', version: '3.3.0', executions: { execution: { id: 'attach-sources', goals: { goal: 'jar' }, }, }, }, { groupId: 'org.apache.maven.plugins', artifactId: 'maven-javadoc-plugin', version: '3.5.0', executions: { execution: { id: 'attach-javadocs', goals: { goal: 'jar' }, }, }, configuration: { failOnError: false, show: 'protected', sourceFileExcludes: { // Excluding the $Module classes so they won't pollute the docsite. They otherwise // are all collected at the top of the classlist, burrying useful information under // a lot of dry scrolling. exclude: ['**/$Module.java'], }, // Adding these makes JavaDoc generation about a 3rd faster (which is far and away the most // expensive part of the build) additionalJOption: [ '-J-XX:+TieredCompilation', '-J-XX:TieredStopAtLevel=1', ], doclint: 'none', quiet: 'true', }, }, { groupId: 'org.apache.maven.plugins', artifactId: 'maven-enforcer-plugin', version: '3.3.0', executions: { execution: { id: 'enforce-maven', goals: { goal: 'enforce' }, configuration: { rules: { requireMavenVersion: { version: '3.6' }, }, }, }, }, }, { groupId: 'org.codehaus.mojo', artifactId: 'versions-maven-plugin', version: '2.16.0', configuration: { generateBackupPoms: false, }, }, ], }, }, }, }, { encoding: 'UTF-8' }) .end({ pretty: true })); this.code.closeFile('pom.xml'); /** * Combines a version number with an optional suffix. The suffix, when present, must begin with * '-' or '.', and will be concatenated as-is to the version number.. * * @param version the semantic version number * @param suffix the suffix, if any. */ function makeVersion(version, suffix) { if (!suffix) { return (0, version_utils_1.toReleaseVersion)(version, index_1.TargetName.JAVA); } if (!suffix.startsWith('-') && !suffix.startsWith('.')) { throw new Error(`versionSuffix must start with '-' or '.', but received ${suffix}`); } return `${version}${suffix}`; } function mavenDependencies() { const dependencies = new Array(); for (const [depName, version] of Object.entries(this.assembly.dependencies ?? {})) { const dep = this.assembly.dependencyClosure?.[depName]; if (!dep?.targets?.java) { throw new Error(`Assembly ${assm.name} depends on ${depName}, which does not declare a java target`); } dependencies.push({ groupId: dep.targets.java.maven.groupId, artifactId: dep.targets.java.maven.artifactId, version: (0, version_utils_1.toMavenVersionRange)(version, dep.targets.java.maven.versionSuffix), }); } // The JSII java runtime base classes dependencies.push({ groupId: 'software.amazon.jsii', artifactId: 'jsii-runtime', version: version_1.VERSION === '0.0.0' ? '[0.0.0-SNAPSHOT]' : // eslint-disable-next-line @typescript-eslint/restrict-template-expressions (0, version_utils_1.toMavenVersionRange)(`^${version_1.VERSION}`), }); // Provides @org.jetbrains.* dependencies.push({ groupId: 'org.jetbrains', artifactId: 'annotations', version: '[16.0.3,20.0.0)', }); // Provides @javax.annotation.Generated for JDKs >= 9 dependencies.push({ '#comment': 'Provides @javax.annotation.Generated for JDKs >= 9', groupId: 'javax.annotation', artifactId: 'javax.annotation-api', version: '[1.3.2,1.4.0)', scope: 'compile', }); return dependencies; } function mavenDevelopers() { return [assm.author, ...(assm.contributors ?? [])].map(toDeveloper); function toDeveloper(person) { const developer = { [person.organization ? 'organization' : 'name']: person.name, roles: { role: person.roles }, }; // We cannot set "undefined" or "null" to a field - this causes invalid XML to be emitted (per POM schema). if (person.email) { developer.email = person.email; } if (person.url) { developer[person.organization ? 'organizationUrl' : 'url'] = person.url; } return developer; } } /** * Get the maven-style license block for a the assembly. * @see https://maven.apache.org/pom.html#Licenses */ function getLicense() { const spdx = spdxLicenseList[assm.license]; return (spdx && { name: spdx.name, url: spdx.url, distribution: 'repo', comments: spdx.osiApproved ? 'An OSI-approved license' : undefined, }); } } emitStaticInitializer(cls) { const consts = (cls.properties ?? []).filter((x) => x.const); if (consts.length === 0) { return; } const javaClass = this.toJavaType(cls); this.code.line(); this.code.openBlock('static'); for (const prop of consts) { const constName = this.renderConstName(prop); const propType = this.toNativeType(prop.type, { forMarshalling: true }); const statement = `software.amazon.jsii.JsiiObject.jsiiStaticGet(${javaClass}.class, "${prop.name}", ${propType})`; this.code.line(`${constName} = ${this.wrapCollection(statement, prop.type, prop.optional)};`); } this.code.closeBlock(); } renderConstName(prop) { return this.code.toSnakeCase(prop.name).toLocaleUpperCase(); // java consts are SNAKE_UPPER_CASE } emitConstProperty(parentType, prop) { const propType = this.toJavaType(prop.type); const propName = this.renderConstName(prop); const access = this.renderAccessLevel(prop); this.code.line(); this.addJavaDocs(prop, { api: 'member', fqn: parentType.fqn, memberName: prop.name, }); this.emitStabilityAnnotations(prop); this.code.line(`${access} final static ${propType} ${propName};`); } emitProperty(cls, prop, definingType, { defaultImpl = false, final = false, includeGetter = true, overrides = !!prop.overrides, } = {}) { const getterType = this.toDecoratedJavaType(prop); const setterTypes = this.toDecoratedJavaTypes(prop, { covariant: prop.static, }); const propName = (0, naming_util_1.jsiiToPascalCase)(JavaGenerator.safeJavaPropertyName(prop.name)); const modifiers = [defaultImpl ? 'default' : this.renderAccessLevel(prop)]; if (prop.static) modifiers.push('static'); if (prop.abstract && !defaultImpl) modifiers.push('abstract'); if (final && !prop.abstract && !defaultImpl) modifiers.push('final'); const javaClass = this.toJavaType(cls); // for unions we only generate overloads for setters, not getters. if (includeGetter) { this.code.line(); this.addJavaDocs(prop, { api: 'member', fqn: definingType.fqn, memberName: prop.name, }); if (overrides && !prop.static) { this.code.line('@Override'); } this.emitStabilityAnnotations(prop); const signature = `${modifiers.join(' ')} ${getterType} get${propName}()`; if (prop.abstract && !defaultImpl) { this.code.line(`${signature};`); } else { this.code.openBlock(signature); let statement; if (prop.static) { statement = `software.amazon.jsii.JsiiObject.jsiiStaticGet(${this.toJavaType(cls)}.class, `; } else { statement = 'software.amazon.jsii.Kernel.get(this, '; } statement += `"${prop.name}", ${this.toNativeType(prop.type, { forMarshalling: true, })})`; this.code.line(`return ${this.wrapCollection(statement, prop.type, prop.optional)};`); this.code.closeBlock(); } } if (!prop.immutable) { for (const type of setterTypes) { this.code.line(); this.addJavaDocs(prop, { api: 'member', fqn: cls.fqn, memberName: prop.name, }); if (overrides && !prop.static) { this.code.line('@Override'); } this.emitStabilityAnnotations(prop); const signature = `${modifiers.join(' ')} void set${propName}(final ${type} value)`; if (prop.abstract && !defaultImpl) { this.code.line(`${signature};`); } else { this.code.openBlock(signature); let statement = ''; // Setters have one overload for each possible type in the union parameter. // If a setter can take a `String | Number`, then we render two setters; // one that takes a string, and one that takes a number. // This allows the compiler to do this type checking for us, // so we should not emit these checks for primitive-only unions. // Also, Java does not allow us to perform these checks if the types // have no overlap (eg if a String instanceof Number). if (type.includes('java.lang.Object') && (!spec.isPrimitiveTypeReference(prop.type) || prop.type.primitive === spec.PrimitiveType.Any)) { this.emitUnionParameterValdation([ { name: 'value', type: this.filterType(prop.type, { covariant: prop.static, optional: prop.optional }, type), }, ]); } if (prop.static) { statement += `software.amazon.jsii.JsiiObject.jsiiStaticSet(${javaClass}.class, `; } else { statement += 'software.amazon.jsii.Kernel.set(this, '; } const value = prop.optional ? 'value' : `java.util.Objects.requireNonNull(value, "${prop.name} is required")`; statement += `"${prop.name}", ${value});`; this.code.line(statement); this.code.closeBlock(); } } } } /** * Filters types from a union to select only those that correspond to the * specified javaType. * * @param ref the type to be filtered. * @param javaType the java type that is expected. * @param covariant whether collections should use the covariant form. * @param optional whether the type at an optional location or not * * @returns a type reference that matches the provided javaType. */ filterType(ref, { covariant, optional }, javaType) { if (!spec.isUnionTypeReference(ref)) { // No filterning needed -- this isn't a type union! return ref; } const types = ref.union.types.filter((t) => this.toDecoratedJavaType({ optional, type: t }, { covariant }) === javaType); assert(types.length > 0, `No type found in ${spec.describeTypeReference(ref)} has Java type ${javaType}`); return { union: { types } }; } emitMethod(cls, method, { defaultImpl = false, final = false, overrides = !!method.overrides, } = {}) { const returnType = method.returns ? this.toDecoratedJavaType(method.returns) : 'void'; const modifiers = [ defaultImpl ? 'default' : this.renderAccessLevel(method), ]; if (method.static) modifiers.push('static'); if (method.abstract && !defaultImpl) modifiers.push('abstract'); if (final && !method.abstract && !defaultImpl) modifiers.push('final'); const async = !!method.async; const methodName = JavaGenerator.safeJavaMethodName(method.name); const signature = `${returnType} ${methodName}(${this.renderMethodParameters(method)})`; this.code.line(); this.addJavaDocs(method, { api: 'member', fqn: cls.fqn, memberName: method.name, }); this.emitStabilityAnnotations(method); if (overrides && !method.static) { this.code.line('@Override'); } if (method.abstract && !defaultImpl) { this.code.line(`${modifiers.join(' ')} ${signature};`); } else { this.code.openBlock(`${modifiers.join(' ')} ${signature}`); this.emitUnionParameterValdation(method.parameters); this.code.line(this.renderMethodCall(cls, method, async)); this.code.closeBlock(); } } /** * Emits type checks for values passed for type union parameters. * * @param parameters the list of parameters received by the function. */ emitUnionParameterValdation(parameters) { if (!this.runtimeTypeChecking) { // We were configured not to emit those, so bail out now. return; } const unionParameters = parameters?.filter(({ type }) => containsUnionType(type)); if (unionParameters == null || unionParameters.length === 0) { return;