packages/jsii-pacmak/lib/targets/java.ts (3,295 lines of code) (raw):

import * as spec from '@jsii/spec'; import * as assert from 'assert'; import * as clone from 'clone'; import { toSnakeCase } from 'codemaker/lib/case-utils'; import { createHash } from 'crypto'; import * as fs from 'fs-extra'; import * as reflect from 'jsii-reflect'; import { RosettaTabletReader, TargetLanguage, enforcesStrictMode, markDownToJavaDoc, ApiLocation, } from 'jsii-rosetta'; import * as path from 'path'; import * as xmlbuilder from 'xmlbuilder'; import { TargetBuilder, BuildOptions } from '../builder'; import { Generator } from '../generator'; import * as logging from '../logging'; import { jsiiToPascalCase } from '../naming-util'; import { JsiiModule } from '../packaging'; import { PackageInfo, Target, findLocalBuildDirs, TargetOptions, } from '../target'; import { shell, Scratch, slugify, setExtend } from '../util'; import { VERSION, VERSION_DESC } from '../version'; import { stabilityPrefixFor, renderSummary } from './_utils'; import { toMavenVersionRange, toReleaseVersion } from './version-utils'; import { TargetName } from './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. */ export class JavaBuilder implements TargetBuilder { private readonly targetName = 'java'; public constructor( private readonly modules: readonly JsiiModule[], private readonly options: BuildOptions, ) {} public async buildModules(): Promise<void> { 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: Array<Scratch<any>> = []; 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 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 Scratch.cleanupAll(scratchDirs); } } catch (e) { logging.warn( `Exception occurred, not cleaning up ${scratchDirs .map((s) => s.directory) .join(', ')}`, ); throw e; } } private async generateModuleCode( module: JsiiModule, options: BuildOptions, where: string, ): Promise<void> { const target = this.makeTarget(module, options); logging.debug(`Generating Java code into ${where}`); await target.generateCode(where, module.tarball); } private async generateAggregateSourceDir( modules: readonly JsiiModule[], options: BuildOptions, ): Promise<Scratch<TemporaryJavaPackage[]>> { return Scratch.make(async (tmpDir: string) => { logging.debug(`Generating aggregate Java source dir at ${tmpDir}`); const ret: TemporaryJavaPackage[] = []; const generatedModules = modules .map((module) => ({ module, relativeName: 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 (const { module, relativeName } of await Promise.all( 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; }); } private async generateAggregatePom(where: string, moduleNames: string[]) { 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_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); } private async copyOutArtifacts( artifactsRoot: string, packages: TemporaryJavaPackage[], ) { 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 */ private outputDir(declaredDir: string) { 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. */ private async generateMavenSettingsForLocalDeps(where: string) { 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<string>(); const resolvedModules = this.modules.map(async (mod) => ({ module: mod, localBuildDirs: await findLocalBuildDirs( mod.moduleDirectory, this.targetName, ), })); for (const { module, localBuildDirs } of await Promise.all( resolvedModules, )) { 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_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; } private makeTarget(module: JsiiModule, options: BuildOptions): Target { 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, }); } } interface TemporaryJavaPackage { /** * Where the sources are (relative to the source root) */ relativeSourceDir: string; /** * Where the artifacts will be stored after build (relative to build dir) */ relativeArtifactsDir: string; /** * Where the artifacts ought to go for this particular module */ outputTargetDirectory: string; } /** * Return the subdirectory of the output directory where the artifacts for this particular package are produced */ function moduleArtifactsSubdir(module: JsiiModule) { const groupId = module.assembly.targets!.java!.maven.groupId; const artifactId = module.assembly.targets!.java!.maven.artifactId; return `${groupId.replace(/\./g, '/')}/${artifactId}`; } export default class Java extends Target { public static toPackageInfos(assm: spec.Assembly): { [language: string]: PackageInfo; } { const groupId = assm.targets!.java!.maven.groupId; const artifactId = assm.targets!.java!.maven.artifactId; const releaseVersion = toReleaseVersion(assm.version, 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}'`, }, }, }; } public static toNativeReference(type: spec.Type, options: any) { const [, ...name] = type.fqn.split('.'); return { java: `import ${[options.package, ...name].join('.')};` }; } protected readonly generator: JavaGenerator; public constructor(options: TargetOptions) { super(options); this.generator = new JavaGenerator(options); } public async build(sourceDir: string, outDir: string): Promise<void> { const url = `file://${outDir}`; const mvnArguments = new Array<string>(); 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 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 }, }, ); } } // ################## // # CODE GENERATOR # // ################## const MODULE_CLASS_NAME = '$Module'; const INTERFACE_PROXY_CLASS_NAME = 'Jsii$Proxy'; const INTERFACE_DEFAULT_CLASS_NAME = 'Jsii$Default'; // Struct that stores metadata about a property that can be used in Java code generation. interface JavaProp { // Documentation for the property docs?: spec.Docs; // The original JSII property spec this struct was derived from spec: spec.Property; // The original JSII type this property was defined on definingType: spec.Type; // Canonical name of the Java property (eg: 'MyProperty') propName: string; // The original canonical name of the JSII property jsiiName: string; // Field name of the Java property (eg: 'myProperty') fieldName: string; // The java type for the property (eg: 'List<String>') fieldJavaType: string; // The java type for the parameter (e.g: 'List<? extends SomeType>') paramJavaType: string; // The NativeType representation of the property's type fieldNativeType: string; // The raw class type of the property that can be used for marshalling (eg: 'List.class') fieldJavaClass: string; // List of types that the property is assignable from. Used to overload setters. javaTypes: string[]; // True if the property is optional. nullable: boolean; // True if the property has been transitively inherited from a base class. inherited: boolean; // True if the property is read-only once initialized. immutable: boolean; } class JavaGenerator extends Generator { // When the code-generator needs to generate code for a property or method that has the same name as a member of this list, the name will // be automatically modified to avoid compile errors. Most of these are java language reserved keywords. In addition to those, any keywords that // are likely to conflict with auto-generated methods or properties (eg: 'build') are also considered reserved. private static readonly RESERVED_KEYWORDS = [ 'abstract', 'assert', 'boolean', 'break', 'build', 'byte', 'case', 'catch', 'char', 'class', 'const', 'continue', 'default', 'double', 'do', 'else', 'enum', 'extends', 'false', 'final', 'finally', 'float', 'for', 'goto', 'if', 'implements', 'import', 'instanceof', 'int', 'interface', 'long', 'native', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'short', 'static', 'strictfp', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try', 'void', 'volatile', 'while', '_', ]; /** * Turns a raw javascript property name (eg: 'default') into a safe Java property name (eg: 'defaultValue'). * @param propertyName the raw JSII property Name */ private static safeJavaPropertyName(propertyName: string) { 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 */ private static safeJavaMethodName(methodName: string) { 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${jsiiToPascalCase(methodName)}`; } return methodName; } /** If false, @Generated will not include generator version nor timestamp */ private emitFullGeneratorInfo?: boolean; private moduleClass!: string; /** * 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. */ private readonly referencedModules: { [name: string]: spec.AssemblyConfiguration; } = {}; private readonly rosetta: RosettaTabletReader; public constructor(options: { readonly rosetta: RosettaTabletReader; readonly runtimeTypeChecking: boolean; }) { super({ ...options, generateOverloadsForMethodWithOptionals: true }); this.rosetta = options.rosetta; } protected onBeginAssembly(assm: spec.Assembly, fingerprint: boolean) { this.emitFullGeneratorInfo = fingerprint; this.moduleClass = this.emitModuleFile(assm); this.emitAssemblyPackageInfo(assm); } protected onEndAssembly(assm: spec.Assembly, fingerprint: boolean) { this.emitMavenPom(assm, fingerprint); delete this.emitFullGeneratorInfo; } protected getAssemblyOutputDir(mod: spec.Assembly) { const dir = this.toNativeFqn(mod.name).replace(/\./g, '/'); return path.join('src', 'main', 'resources', dir); } protected onBeginClass(cls: spec.ClassType, abstract: boolean) { 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); } protected onEndClass(cls: spec.ClassType) { if (cls.abstract) { const type = this.reflectAssembly.findType(cls.fqn) as reflect.ClassType; this.emitProxy(type); } else { this.emitClassBuilder(cls); } this.code.closeBlock(); this.closeFileIfNeeded(cls); } protected onInitializer(cls: spec.ClassType, method: spec.Initializer) { 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(); } protected onInitializerOverload( cls: spec.ClassType, overload: spec.Method, _originalInitializer: spec.Method, ) { this.onInitializer(cls, overload); } protected onField( _cls: spec.ClassType, _prop: spec.Property, _union?: spec.UnionTypeReference, ) { /* noop */ } protected onProperty(cls: spec.ClassType, prop: spec.Property) { this.emitProperty(cls, prop, cls); } protected onStaticProperty(cls: spec.ClassType, prop: spec.Property) { 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. */ protected onUnionProperty( cls: spec.ClassType, prop: spec.Property, _union: spec.UnionTypeReference, ) { this.emitProperty(cls, prop, cls); } protected onMethod(cls: spec.ClassType, method: spec.Method) { this.emitMethod(cls, method); } protected onMethodOverload( cls: spec.ClassType, overload: spec.Method, _originalMethod: spec.Method, ) { this.onMethod(cls, overload); } protected onStaticMethod(cls: spec.ClassType, method: spec.Method) { this.emitMethod(cls, method); } protected onStaticMethodOverload( cls: spec.ClassType, overload: spec.Method, _originalMethod: spec.Method, ) { this.emitMethod(cls, overload); } protected onBeginEnum(enm: spec.EnumType) { 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}`); } protected onEndEnum(enm: spec.EnumType) { this.code.closeBlock(); this.closeFileIfNeeded(enm); } protected onEnumMember(parentType: spec.EnumType, member: spec.EnumMember) { 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 */ protected onBeginNamespace(ns: string) { const submodule = this.assembly.submodules?.[ns]; if (submodule) { this.emitSubmodulePackageInfo(this.assembly, ns); } } protected onEndNamespace(_ns: string) { /* noop */ } protected onBeginInterface(ifc: spec.InterfaceType) { 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}`, ); } protected onEndInterface(ifc: spec.InterfaceType) { this.emitMultiplyInheritedOptionalProperties(ifc); if (ifc.datatype) { this.emitDataType(ifc); } else { const type = this.reflectAssembly.findType( ifc.fqn, ) as reflect.InterfaceType; 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); } protected onInterfaceMethod(ifc: spec.InterfaceType, method: spec.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)});`, ); } protected onInterfaceMethodOverload( ifc: spec.InterfaceType, overload: spec.Method, _originalMethod: spec.Method, ) { this.onInterfaceMethod(ifc, overload); } protected onInterfaceProperty(ifc: spec.InterfaceType, prop: spec.Property) { const getterType = this.toDecoratedJavaType(prop); const propName = 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 */ private emitMultiplyInheritedOptionalProperties(ifc: spec.InterfaceType) { 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; }, {} as Record<string, { readonly spec: spec.Property; count: number }>, ); 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(this: JavaGenerator, fqn: string) { const type = this.findType(fqn) as spec.InterfaceType; const result: Record<string, spec.Property> = {}; 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; } } private emitAssemblyPackageInfo(mod: spec.Assembly) { 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); } private emitSubmodulePackageInfo(assembly: spec.Assembly, moduleFqn: string) { 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); } private emitMavenPom(assm: spec.Assembly, fingerprint: boolean) { 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_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: string, suffix?: string): string { if (!suffix) { return toReleaseVersion(version, TargetName.JAVA); } if (!suffix.startsWith('-') && !suffix.startsWith('.')) { throw new Error( `versionSuffix must start with '-' or '.', but received ${suffix}`, ); } return `${version}${suffix}`; } function mavenDependencies(this: JavaGenerator) { const dependencies = new Array<MavenDependency>(); 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: toMavenVersionRange( version, dep.targets.java.maven.versionSuffix, ), }); } // The JSII java runtime base classes dependencies.push({ groupId: 'software.amazon.jsii', artifactId: 'jsii-runtime', version: VERSION === '0.0.0' ? '[0.0.0-SNAPSHOT]' : // eslint-disable-next-line @typescript-eslint/restrict-template-expressions toMavenVersionRange(`^${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: spec.Person) { const developer: any = { [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, } ); } } private emitStaticInitializer(cls: spec.ClassType) { 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(); } private renderConstName(prop: spec.Property) { return this.code.toSnakeCase(prop.name).toLocaleUpperCase(); // java consts are SNAKE_UPPER_CASE } private emitConstProperty(parentType: spec.Type, prop: spec.Property) { 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};`); } private emitProperty( cls: spec.Type, prop: spec.Property, definingType: spec.Type, { defaultImpl = false, final = false, includeGetter = true, overrides = !!prop.overrides, }: { defaultImpl?: boolean; final?: boolean; includeGetter?: boolean; overrides?: boolean; } = {}, ) { const getterType = this.toDecoratedJavaType(prop); const setterTypes = this.toDecoratedJavaTypes(prop, { covariant: prop.static, }); const propName = 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. */ private filterType( ref: spec.TypeReference, { covariant, optional }: { covariant?: boolean; optional?: boolean }, javaType: string, ): spec.TypeReference { 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 } }; } private emitMethod( cls: spec.Type, method: spec.Method, { defaultImpl = false, final = false, overrides = !!method.overrides, }: { defaultImpl?: boolean; final?: boolean; overrides?: boolean } = {}, ) { 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. */ private emitUnionParameterValdation( parameters?: readonly spec.Parameter[], ): void { 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; } this.code.openBlock( 'if (software.amazon.jsii.Configuration.getRuntimeTypeChecking())', ); for (const param of unionParameters) { if (param.variadic) { const javaType = this.toJavaType(param.type); const asListName = `__${param.name}__asList`; this.code.line( `final java.util.List<${javaType}> ${asListName} = java.util.Arrays.asList(${param.name});`, ); validate.call( this, asListName, `.append("${param.name}")`, { collection: { kind: spec.CollectionKind.Array, elementtype: param.type, }, }, param.name, true, ); } else { validate.call( this, param.name, `.append("${param.name}")`, param.type, param.name, ); } } this.code.closeBlock(); function validate( this: JavaGenerator, value: string, descr: string, type: spec.TypeReference, parameterName: string, isRawArray = false, ) { if (spec.isUnionTypeReference(type)) { validateTypeUnion.call(this, value, descr, type, parameterName); } else if (spec.isCollectionTypeReference(type)) { switch (type.collection.kind) { case spec.CollectionKind.Array: return validateArray.call( this, value, descr, type.collection.elementtype, parameterName, isRawArray, ); case spec.CollectionKind.Map: return validateMap.call( this, value, descr, type.collection.elementtype, parameterName, ); default: throw new Error( `Unhandled collection kind: ${spec.describeTypeReference(type)}`, ); } } } function validateArray( this: JavaGenerator, value: string, descr: string, elementType: spec.TypeReference, parameterName: string, isRawArray = false, ) { const suffix = createHash('sha256') .update(descr) .digest('hex') .slice(0, 6); const idxName = `__idx_${suffix}`; const valName = `__val_${suffix}`; this.code.openBlock( `for (int ${idxName} = 0; ${idxName} < ${value}.size(); ${idxName}++)`, ); const eltType = this.toJavaType(elementType); this.code.line(`final ${eltType} ${valName} = ${value}.get(${idxName});`); validate.call( this, valName, isRawArray ? `${descr}.append("[").append(${idxName}).append("]")` : `${descr}.append(".get(").append(${idxName}).append(")")`, elementType, parameterName, ); this.code.closeBlock(); } function validateMap( this: JavaGenerator, value: string, descr: string, elementType: spec.TypeReference, parameterName: string, ) { // we have to perform this check before the loop, // because the loop will assume that the keys are Strings; // this throws a ClassCastException this.code.openBlock( `if (!(${value}.keySet().toArray()[0] instanceof String))`, ); this.code.indent(`throw new IllegalArgumentException(`); this.code.indent(`new java.lang.StringBuilder("Expected ")`); this.code.line(`${descr}.append(".keySet()")`); this.code.line(`.append(" to contain class String; received ")`); this.code.line( `.append(${value}.keySet().toArray()[0].getClass()).toString());`, ); this.code.unindent(false); this.code.unindent(false); this.code.closeBlock(); const suffix = createHash('sha256') .update(descr) .digest('hex') .slice(0, 6); const varName = `__item_${suffix}`; const valName = `__val_${suffix}`; const javaElemType = this.toJavaType(elementType); this.code.openBlock( `for (final java.util.Map.Entry<String, ${javaElemType}> ${varName}: ${value}.entrySet())`, ); this.code.line( `final ${javaElemType} ${valName} = ${varName}.getValue();`, ); validate.call( this, valName, `${descr}.append(".get(\\"").append((${varName}.getKey())).append("\\")")`, elementType, parameterName, ); this.code.closeBlock(); } function validateTypeUnion( this: JavaGenerator, value: string, descr: string, type: spec.UnionTypeReference, parameterName: string, ) { let emitAnd = false; const nestedCollectionUnionTypes = new Map<string, spec.TypeReference>(); const typeRefs = type.union.types; if (typeRefs.length > 1) { this.code.indent('if ('); } const checked = new Set<string>(); for (const typeRef of typeRefs) { const prefix = emitAnd ? '&&' : ''; const javaRawType = this.toJavaTypeNoGenerics(typeRef); if (checked.has(javaRawType)) { continue; } else { checked.add(javaRawType); } const javaType = this.toJavaType(typeRef); if (javaRawType !== javaType) { nestedCollectionUnionTypes.set(javaType, typeRef); } const test = `${value} instanceof ${javaRawType}`; if (typeRefs.length > 1) { this.code.line(`${prefix} !(${test})`); } emitAnd = true; } if ( typeRefs.length > 1 && typeRefs.some( (t) => spec.isNamedTypeReference(t) && spec.isInterfaceType(this.findType(t.fqn)), ) ) { // Only anonymous objects at runtime can be `JsiiObject`s. this.code.line( `&& !(${value}.getClass().equals(software.amazon.jsii.JsiiObject.class))`, ); } if (typeRefs.length > 1) { this.code.unindent(false); this.code.openBlock(')'); const placeholders = typeRefs .map((typeRef) => { return `${this.toJavaType(typeRef)}`; }) .join(', '); this.code.indent(`throw new IllegalArgumentException(`); this.code.indent(`new java.lang.StringBuilder("Expected ")`); this.code.line(descr); this.code.line(`.append(" to be one of: ${placeholders}; received ")`); this.code.line(`.append(${value}.getClass()).toString());`); this.code.unindent(false); this.code.unindent(false); this.code.closeBlock(); } for (const [javaType, typeRef] of nestedCollectionUnionTypes) { const varName = typeRefs.length > 1 ? `__cast_${createHash('sha256') .update(value) .digest('hex') .slice(0, 6)}` : value; if (typeRefs.length > 1) { this.code.openBlock( `if (${value} instanceof ${this.toJavaTypeNoGenerics(typeRef)})`, ); this.code.line(`@SuppressWarnings("unchecked")`); this.code.line( `final ${javaType} ${varName} = (${javaType})${value};`, ); } validate.call(this, varName, descr, typeRef, parameterName); if (typeRefs.length > 1) { this.code.closeBlock(); } } } } /** * We are now going to build a class that can be used as a proxy for untyped * javascript objects that implement this interface. we want java code to be * able to interact with them, so we will create a proxy class which * implements this interface and has the same methods. * * These proxies are also used to extend abstract classes to allow the JSII * engine to instantiate an abstract class in Java. */ private emitProxy(type: reflect.InterfaceType | reflect.ClassType) { const name = INTERFACE_PROXY_CLASS_NAME; this.code.line(); this.code.line('/**'); this.code.line( ' * A proxy class which represents a concrete javascript instance of this type.', ); this.code.line(' */'); const baseInterfaces = this.defaultInterfacesFor(type, { includeThisType: true, }); if (type.isInterfaceType() && !hasDefaultInterfaces(type.assembly)) { // Extend this interface directly since this module does not have the Jsii$Default baseInterfaces.push(this.toNativeFqn(type.fqn)); } const suffix = type.isInterfaceType() ? `extends software.amazon.jsii.JsiiObject implements ${baseInterfaces.join( ', ', )}` : `extends ${this.toNativeFqn(type.fqn)}${ baseInterfaces.length > 0 ? ` implements ${baseInterfaces.join(', ')}` : '' }`; const modifiers = type.isInterfaceType() ? 'final' : 'private static final'; this.code.line(ANN_INTERNAL); this.code.openBlock(`${modifiers} class ${name} ${suffix}`); this.code.openBlock( `protected ${name}(final software.amazon.jsii.JsiiObjectRef objRef)`, ); this.code.line('super(objRef);'); this.code.closeBlock(); // emit all properties for (const reflectProp of type.allProperties.filter( (prop) => prop.abstract && (prop.parentType.fqn === type.fqn || prop.parentType.isClassType() || !hasDefaultInterfaces(prop.assembly)), )) { const prop = clone(reflectProp.spec); prop.abstract = false; // Emitting "final" since this is a proxy and nothing will/should override this this.emitProperty(type.spec, prop, reflectProp.definingType.spec, { final: true, overrides: true, }); } // emit all the methods for (const reflectMethod of type.allMethods.filter( (method) => method.abstract && (method.parentType.fqn === type.fqn || method.parentType.isClassType() || !hasDefaultInterfaces(method.assembly)), )) { const method = clone(reflectMethod.spec); method.abstract = false; // Emitting "final" since this is a proxy and nothing will/should override this this.emitMethod(type.spec, method, { final: true, overrides: true }); for (const overloadedMethod of this.createOverloadsForOptionals(method)) { overloadedMethod.abstract = false; this.emitMethod(type.spec, overloadedMethod, { final: true, overrides: true, }); } } this.code.closeBlock(); } private emitDefaultImplementation(type: reflect.InterfaceType) { const baseInterfaces = [type.name, ...this.defaultInterfacesFor(type)]; this.code.line(); this.code.line('/**'); this.code.line( ` * Internal default implementation for {@link ${type.name}}.`, ); this.code.line(' */'); this.code.line(ANN_INTERNAL); this.code.openBlock( `interface ${INTERFACE_DEFAULT_CLASS_NAME} extends ${baseInterfaces .sort() .join(', ')}`, ); for (const property of type.allProperties.filter( (prop) => prop.abstract && // Only checking the getter - java.lang.Object has no setters. !isJavaLangObjectMethodName(`get${jsiiToPascalCase(prop.name)}`) && (prop.parentType.fqn === type.fqn || !hasDefaultInterfaces(prop.assembly)), )) { this.emitProperty(type.spec, property.spec, property.definingType.spec, { defaultImpl: true, overrides: type.isInterfaceType(), }); } for (const method of type.allMethods.filter( (method) => method.abstract && !isJavaLangObjectMethodName(method.name) && (method.parentType.fqn === type.fqn || !hasDefaultInterfaces(method.assembly)), )) { this.emitMethod(type.spec, method.spec, { defaultImpl: true, overrides: type.isInterfaceType(), }); } this.code.closeBlock(); } private emitStabilityAnnotations(entity: spec.Documentable) { if (!entity.docs) { return; } if (entity.docs.stability) { this.code.line( `@software.amazon.jsii.Stability(software.amazon.jsii.Stability.Level.${_level( entity.docs.stability, )})`, ); } if ( entity.docs.stability === spec.Stability.Deprecated || entity.docs.deprecated ) { this.code.line('@Deprecated'); } function _level(stability: spec.Stability): string { switch (stability) { case spec.Stability.Deprecated: return 'Deprecated'; case spec.Stability.Experimental: return 'Experimental'; case spec.Stability.External: // Rendering 'External' out as publicly visible state is confusing. As far // as users are concerned we just advertise this as stable. return 'Stable'; case spec.Stability.Stable: return 'Stable'; default: throw new Error(`Unexpected stability: ${stability as any}`); } } } private toJavaProp( property: spec.Property, definingType: spec.Type, inherited: boolean, ): JavaProp { const safeName = JavaGenerator.safeJavaPropertyName(property.name); const propName = jsiiToPascalCase(safeName); return { docs: property.docs, spec: property, definingType, propName, jsiiName: property.name, nullable: !!property.optional, fieldName: this.code.toCamelCase(safeName), fieldJavaType: this.toJavaType(property.type), paramJavaType: this.toJavaType(property.type, { covariant: true }), fieldNativeType: this.toNativeType(property.type), fieldJavaClass: `${this.toJavaType(property.type, { forMarshalling: true, })}.class`, javaTypes: this.toJavaTypes(property.type, { covariant: true }), immutable: property.immutable ?? false, inherited, }; } private emitClassBuilder(cls: spec.ClassType) { // Not rendering if there is no initializer, or if the initializer is protected or variadic if (cls.initializer == null || cls.initializer.protected) { return; } // Not rendering if the initializer has no parameters if (cls.initializer.parameters == null) { return; } // Not rendering if there is a nested "Builder" class if ( this.reflectAssembly.tryFindType(`${cls.fqn}.${BUILDER_CLASS_NAME}`) != null ) { return; } // Find the first struct parameter of the constructor (if any) const firstStruct = cls.initializer.parameters.find((param) => { if (!spec.isNamedTypeReference(param.type)) { return false; } const paramType = this.reflectAssembly.tryFindType(param.type.fqn); return paramType?.isDataType(); }); // Not rendering if there is no struct parameter if (firstStruct == null) { return; } const structType = this.reflectAssembly.findType( (firstStruct.type as spec.NamedTypeReference).fqn, ) as reflect.InterfaceType; const structParamName = this.code.toCamelCase( JavaGenerator.safeJavaPropertyName(firstStruct.name), ); const structBuilder = `${this.toJavaType( firstStruct.type, )}.${BUILDER_CLASS_NAME}`; const positionalParams = cls.initializer.parameters .filter((p) => p !== firstStruct) .map((param) => ({ param, fieldName: this.code.toCamelCase( JavaGenerator.safeJavaPropertyName(param.name), ), javaType: this.toJavaType(param.type), })); const builtType = this.toJavaType(cls); this.code.line(); this.code.line('/**'); // eslint-disable-next-line prettier/prettier this.code.line( ` * ${stabilityPrefixFor( cls.initializer, )}A fluent builder for {@link ${builtType}}.`, ); this.code.line(' */'); this.emitStabilityAnnotations(cls.initializer); this.code.openBlock( `public static final class ${BUILDER_CLASS_NAME} implements software.amazon.jsii.Builder<${builtType}>`, ); // Static factory method(s) for (const params of computeOverrides(positionalParams)) { const dummyMethod: spec.Method = { docs: { stability: cls.initializer.docs?.stability ?? cls.docs?.stability, returns: `a new instance of {@link ${BUILDER_CLASS_NAME}}.`, }, name: 'create', parameters: params.map((param) => param.param), }; this.addJavaDocs(dummyMethod, { api: 'member', fqn: cls.fqn, memberName: dummyMethod.name, }); this.emitStabilityAnnotations(cls.initializer); this.code.openBlock( `public static ${BUILDER_CLASS_NAME} create(${params .map( (param) => `final ${param.javaType}${param.param.variadic ? '...' : ''} ${ param.fieldName }`, ) .join(', ')})`, ); this.code.line( `return new ${BUILDER_CLASS_NAME}(${positionalParams .map((param, idx) => (idx < params.length ? param.fieldName : 'null')) .join(', ')});`, ); this.code.closeBlock(); } // Private properties this.code.line(); for (const param of positionalParams) { this.code.line( `private final ${param.javaType}${param.param.variadic ? '[]' : ''} ${ param.fieldName };`, ); } this.code.line( `private ${ firstStruct.optional ? '' : 'final ' }${structBuilder} ${structParamName};`, ); // Private constructor this.code.line(); this.code.openBlock( `private ${BUILDER_CLASS_NAME}(${positionalParams .map( (param) => `final ${param.javaType}${param.param.variadic ? '...' : ''} ${ param.fieldName }`, ) .join(', ')})`, ); for (const param of positionalParams) { this.code.line(`this.${param.fieldName} = ${param.fieldName};`); } if (!firstStruct.optional) { this.code.line(`this.${structParamName} = new ${structBuilder}();`); } this.code.closeBlock(); // Fields for (const prop of structType.allProperties) { const fieldName = this.code.toCamelCase( JavaGenerator.safeJavaPropertyName(prop.name), ); this.code.line(); const setter: spec.Method = { name: fieldName, docs: { ...prop.spec.docs, stability: prop.spec.docs?.stability, returns: '{@code this}', }, parameters: [ { name: fieldName, type: spec.CANONICAL_ANY, // We don't quite care in this context! docs: prop.spec.docs, }, ], }; for (const javaType of this.toJavaTypes(prop.type.spec!, { covariant: true, })) { this.addJavaDocs(setter, { api: 'member', fqn: prop.definingType.fqn, // Could be inherited memberName: prop.name, }); this.emitStabilityAnnotations(prop.spec); this.code.openBlock( `public ${BUILDER_CLASS_NAME} ${fieldName}(final ${javaType} ${fieldName})`, ); this.code.line( `this.${structParamName}${ firstStruct.optional ? '()' : '' }.${fieldName}(${fieldName});`, ); this.code.line('return this;'); this.code.closeBlock(); } } // Final build method this.code.line(); this.code.line('/**'); this.code.line( ` * @return a newly built instance of {@link ${builtType}}.`, ); this.code.line(' */'); this.emitStabilityAnnotations(cls.initializer); this.code.line('@Override'); this.code.openBlock(`public ${builtType} build()`); const params = cls.initializer.parameters.map((param) => { if (param === firstStruct) { return firstStruct.optional ? `this.${structParamName} != null ? this.${structParamName}.build() : null` : `this.${structParamName}.build()`; } return `this.${ positionalParams.find((p) => param === p.param)!.fieldName }`; }); this.code.indent(`return new ${builtType}(`); params.forEach((param, idx) => this.code.line(`${param}${idx < params.length - 1 ? ',' : ''}`), ); this.code.unindent(');'); this.code.closeBlock(); // Optional builder initialization if (firstStruct.optional) { this.code.line(); this.code.openBlock(`private ${structBuilder} ${structParamName}()`); this.code.openBlock(`if (this.${structParamName} == null)`); this.code.line(`this.${structParamName} = new ${structBuilder}();`); this.code.closeBlock(); this.code.line(`return this.${structParamName};`); this.code.closeBlock(); } this.code.closeBlock(); } private emitBuilderSetter( prop: JavaProp, builderName: string, parentType: spec.InterfaceType, ) { for (const type of prop.javaTypes) { this.code.line(); this.code.line('/**'); this.code.line( ` * Sets the value of {@link ${parentType.name}#${getterFor( prop.fieldName, )}}`, ); const summary = prop.docs?.summary ?? 'the value to be set'; this.code.line( ` * ${paramJavadoc(prop.fieldName, prop.nullable, summary)}`, ); if (prop.docs?.remarks != null) { const indent = ' '.repeat(7 + prop.fieldName.length); const remarks = myMarkDownToJavaDoc( this.convertSamplesInMarkdown(prop.docs.remarks, { api: 'member', fqn: prop.definingType.fqn, memberName: prop.jsiiName, }), ).trimRight(); for (const line of remarks.split('\n')) { this.code.line(` * ${indent} ${line}`); } } this.code.line(' * @return {@code this}'); if (prop.docs?.deprecated) { this.code.line(` * @deprecated ${prop.docs.deprecated}`); } this.code.line(' */'); this.emitStabilityAnnotations(prop.spec); // We add an explicit cast if both types are generic but they are not identical (one is covariant, the other isn't) const explicitCast = type.includes('<') && prop.fieldJavaType.includes('<') && type !== prop.fieldJavaType ? `(${prop.fieldJavaType})` : ''; if (explicitCast !== '') { // We'll be doing a safe, but unchecked cast, so suppress that warning this.code.line('@SuppressWarnings("unchecked")'); } this.code.openBlock( `public ${builderName} ${prop.fieldName}(${type} ${prop.fieldName})`, ); this.code.line( `this.${prop.fieldName} = ${explicitCast}${prop.fieldName};`, ); this.code.line('return this;'); this.code.closeBlock(); } function getterFor(fieldName: string): string { const [first, ...rest] = fieldName; return `get${first.toUpperCase()}${rest.join('')}`; } } private emitInterfaceBuilder( classSpec: spec.InterfaceType, constructorName: string, props: JavaProp[], ) { // Start builder() this.code.line(); this.code.line('/**'); this.code.line( ` * @return a {@link ${BUILDER_CLASS_NAME}} of {@link ${classSpec.name}}`, ); this.code.line(' */'); this.emitStabilityAnnotations(classSpec); this.code.openBlock(`static ${BUILDER_CLASS_NAME} builder()`); this.code.line(`return new ${BUILDER_CLASS_NAME}();`); this.code.closeBlock(); // End builder() // Start Builder this.code.line('/**'); this.code.line(` * A builder for {@link ${classSpec.name}}`); this.code.line(' */'); this.emitStabilityAnnotations(classSpec); this.code.openBlock( `public static final class ${BUILDER_CLASS_NAME} implements software.amazon.jsii.Builder<${classSpec.name}>`, ); props.forEach((prop) => this.code.line(`${prop.fieldJavaType} ${prop.fieldName};`), ); props.forEach((prop) => this.emitBuilderSetter(prop, BUILDER_CLASS_NAME, classSpec), ); // Start build() this.code.line(); this.code.line('/**'); this.code.line(' * Builds the configured instance.'); this.code.line(` * @return a new instance of {@link ${classSpec.name}}`); this.code.line( ' * @throws NullPointerException if any required attribute was not provided', ); this.code.line(' */'); this.emitStabilityAnnotations(classSpec); this.code.line('@Override'); this.code.openBlock(`public ${classSpec.name} build()`); this.code.line(`return new ${constructorName}(this);`); this.code.closeBlock(); // End build() this.code.closeBlock(); // End Builder } private emitDataType(ifc: spec.InterfaceType) { // collect all properties from all base structs and dedupe by name. It is assumed that the generation of the // assembly will not permit multiple overloaded inherited properties with the same name and that this will be // enforced by Typescript constraints. const propsByName: { [name: string]: JavaProp } = {}; function collectProps( this: JavaGenerator, currentIfc: spec.InterfaceType, isBaseClass = false, ) { for (const property of currentIfc.properties ?? []) { const javaProp = this.toJavaProp(property, currentIfc, isBaseClass); propsByName[javaProp.propName] = javaProp; } // add props of base struct for (const base of currentIfc.interfaces ?? []) { collectProps.call( this, this.findType(base) as spec.InterfaceType, true, ); } } collectProps.call(this, ifc); const props = Object.values(propsByName); this.emitInterfaceBuilder(ifc, INTERFACE_PROXY_CLASS_NAME, props); // Start implementation class this.code.line(); this.code.line('/**'); this.code.line(` * An implementation for {@link ${ifc.name}}`); this.code.line(' */'); this.emitStabilityAnnotations(ifc); this.code.line(ANN_INTERNAL); this.code.openBlock( `final class ${INTERFACE_PROXY_CLASS_NAME} extends software.amazon.jsii.JsiiObject implements ${ifc.name}`, ); // Immutable properties props.forEach((prop) => this.code.line(`private final ${prop.fieldJavaType} ${prop.fieldName};`), ); // Start JSII reference constructor this.code.line(); this.code.line('/**'); this.code.line( ' * Constructor that initializes the object based on values retrieved from the JsiiObject.', ); this.code.line(' * @param objRef Reference to the JSII managed object.'); this.code.line(' */'); this.code.openBlock( `protected ${INTERFACE_PROXY_CLASS_NAME}(final software.amazon.jsii.JsiiObjectRef objRef)`, ); this.code.line('super(objRef);'); props.forEach((prop) => this.code.line( `this.${prop.fieldName} = software.amazon.jsii.Kernel.get(this, "${prop.jsiiName}", ${prop.fieldNativeType});`, ), ); this.code.closeBlock(); // End JSII reference constructor // Start builder constructor this.code.line(); this.code.line('/**'); this.code.line( ' * Constructor that initializes the object based on literal property values passed by the {@link Builder}.', ); this.code.line(' */'); if (props.some((prop) => prop.fieldJavaType !== prop.paramJavaType)) { this.code.line('@SuppressWarnings("unchecked")'); } this.code.openBlock( `protected ${INTERFACE_PROXY_CLASS_NAME}(final ${BUILDER_CLASS_NAME} builder)`, ); this.code.line( 'super(software.amazon.jsii.JsiiObject.InitializationMode.JSII);', ); props.forEach((prop) => { const explicitCast = prop.fieldJavaType !== prop.paramJavaType ? `(${prop.fieldJavaType})` : ''; this.code.line( `this.${prop.fieldName} = ${explicitCast}${_validateIfNonOptional( `builder.${prop.fieldName}`, prop, )};`, ); }); this.code.closeBlock(); // End literal constructor // Getters props.forEach((prop) => { this.code.line(); this.code.line('@Override'); this.code.openBlock( `public final ${prop.fieldJavaType} get${prop.propName}()`, ); this.code.line(`return this.${prop.fieldName};`); this.code.closeBlock(); }); // emit $jsii$toJson which will be called to serialize this object when sent to JS this.code.line(); this.code.line('@Override'); this.code.line(ANN_INTERNAL); this.code.openBlock( 'public com.fasterxml.jackson.databind.JsonNode $jsii$toJson()', ); this.code.line( 'final com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE;', ); this.code.line( `final com.fasterxml.jackson.databind.node.ObjectNode data = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode();`, ); this.code.line(); for (const prop of props) { if (prop.nullable) { this.code.openBlock(`if (this.get${prop.propName}() != null)`); } this.code.line( `data.set("${prop.spec.name}", om.valueToTree(this.get${prop.propName}()));`, ); if (prop.nullable) { this.code.closeBlock(); } } this.code.line(); this.code.line( 'final com.fasterxml.jackson.databind.node.ObjectNode struct = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode();', ); this.code.line(`struct.set("fqn", om.valueToTree("${ifc.fqn}"));`); this.code.line('struct.set("data", data);'); this.code.line(); this.code.line( 'final com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode();', ); this.code.line('obj.set("$jsii.struct", struct);'); this.code.line(); this.code.line('return obj;'); this.code.closeBlock(); // End $jsii$toJson // Generate equals() override this.emitEqualsOverride(ifc.name, props); // Generate hashCode() override this.emitHashCodeOverride(props); this.code.closeBlock(); // End implementation class function _validateIfNonOptional(variable: string, prop: JavaProp): string { if (prop.nullable) { return variable; } return `java.util.Objects.requireNonNull(${variable}, "${prop.fieldName} is required")`; } } private emitEqualsOverride(className: string, props: JavaProp[]) { // A class without properties does not need to override equals() if (props.length === 0) { return; } this.code.line(); this.code.line('@Override'); this.code.openBlock('public final boolean equals(final Object o)'); this.code.line('if (this == o) return true;'); // This was already checked by `super.equals(o)`, so we skip it here... this.code.line( 'if (o == null || getClass() != o.getClass()) return false;', ); this.code.line(); this.code.line( `${className}.${INTERFACE_PROXY_CLASS_NAME} that = (${className}.${INTERFACE_PROXY_CLASS_NAME}) o;`, ); this.code.line(); const initialProps = props.slice(0, props.length - 1); const finalProp = props[props.length - 1]; initialProps.forEach((prop) => { const predicate = prop.nullable ? `this.${prop.fieldName} != null ? !this.${prop.fieldName}.equals(that.${prop.fieldName}) : that.${prop.fieldName} != null` : `!${prop.fieldName}.equals(that.${prop.fieldName})`; this.code.line(`if (${predicate}) return false;`); }); // The final (returned predicate) is the inverse of the other ones const finalPredicate = finalProp.nullable ? `this.${finalProp.fieldName} != null ? this.${finalProp.fieldName}.equals(that.${finalProp.fieldName}) : ` + `that.${finalProp.fieldName} == null` : `this.${finalProp.fieldName}.equals(that.${finalProp.fieldName})`; this.code.line(`return ${finalPredicate};`); this.code.closeBlock(); } private emitHashCodeOverride(props: JavaProp[]) { // A class without properties does not need to override hashCode() if (props.length === 0) { return; } this.code.line(); this.code.line('@Override'); this.code.openBlock('public final int hashCode()'); const firstProp = props[0]; const remainingProps = props.slice(1); this.code.line(`int result = ${_hashCodeForProp(firstProp)};`); remainingProps.forEach((prop) => this.code.line(`result = 31 * result + (${_hashCodeForProp(prop)});`), ); this.code.line('return result;'); this.code.closeBlock(); function _hashCodeForProp(prop: JavaProp) { return prop.nullable ? `this.${prop.fieldName} != null ? this.${prop.fieldName}.hashCode() : 0` : `this.${prop.fieldName}.hashCode()`; } } private openFileIfNeeded(type: spec.Type) { if (this.isNested(type)) { return; } this.code.openFile(this.toJavaFilePath(this.assembly, type)); this.code.line( `package ${this.toNativeName(this.assembly, type).packageName};`, ); this.code.line(); } private closeFileIfNeeded(type: spec.Type) { if (this.isNested(type)) { return; } this.code.closeFile(this.toJavaFilePath(this.assembly, type)); } private isNested(type: spec.Type) { if (!this.assembly.types || !type.namespace) { return false; } const parent = `${type.assembly}.${type.namespace}`; return parent in this.assembly.types; } private toJavaFilePath(assm: spec.Assembly, fqn: string): string; private toJavaFilePath(assm: spec.Assembly, type: spec.Type): string; private toJavaFilePath(assm: spec.Assembly, what: spec.Type | string) { const fqn = typeof what === 'string' ? what : what.fqn; const { packageName, typeNames } = translateFqn(assm, fqn); if (typeNames.length === 0) { throw new Error( `toJavaFilePath: ${fqn} must indicate a type, but doesn't: ${JSON.stringify( { packageName, typeNames }, )}`, ); } return `${path.join( 'src', 'main', 'java', ...packageName.split('.'), typeNames[0], )}.java`; } private toJavaResourcePath(assm: spec.Assembly, fqn: string, ext = '.txt') { const { packageName, typeName } = this.toNativeName(assm, { fqn, kind: spec.TypeKind.Class, assembly: assm.name, name: fqn.replace(/.*\.([^.]+)$/, '$1'), }); const parts = [ ...packageName.split('.'), `${typeName.split('.')[0]}${ext}`, ]; // Resource names are /-delimited paths (even on Windows *wink wink*) const name = parts.join('/'); const filePath = path.join('src', 'main', 'resources', ...parts); return { filePath, name }; } // eslint-disable-next-line complexity private addJavaDocs( doc: spec.Documentable, apiLoc: ApiLocation, defaultText?: string, ) { if ( !defaultText && Object.keys(doc.docs ?? {}).length === 0 && !((doc as spec.Method).parameters ?? []).some( (p) => Object.keys(p.docs ?? {}).length !== 0, ) ) { return; } const docs = (doc.docs = doc.docs ?? {}); const paras = []; if (docs.summary) { paras.push(stripNewLines(myMarkDownToJavaDoc(renderSummary(docs)))); } else if (defaultText) { paras.push(myMarkDownToJavaDoc(defaultText)); } if (docs.remarks) { paras.push( myMarkDownToJavaDoc( this.convertSamplesInMarkdown(docs.remarks, apiLoc), ).trimRight(), ); } if (docs.default) { paras.push(`Default: ${docs.default}`); // NOTE: there is no annotation in JavaDoc for this } if (docs.example) { paras.push('Example:'); const convertedExample = this.convertExample(docs.example, apiLoc); // We used to use the slightly nicer `<pre>{@code ...}</pre>`, which doesn't // require (and therefore also doesn't allow) escaping special characters. // // However, code samples may contain block quotes of their own ('*/') that // would terminate the block comment from the PoV of the Java tokenizer, and // since `{@code ... }` doesn't allow any escaping, there's also no way to encode // '*/' in a way that would (a) hide it from the tokenizer and (b) give '*/' back // after processing JavaDocs. // // Hence, we just resort to HTML-encoding everything (same as we do for code // examples that have been translated from MarkDown). paras.push( myMarkDownToJavaDoc(['```', convertedExample, '```'].join('\n')), ); } const tagLines = []; if (docs.returns) { tagLines.push(`@return ${myMarkDownToJavaDoc(docs.returns)}`); } if (docs.see) { tagLines.push( `@see <a href="${escape(docs.see)}">${escape(docs.see)}</a>`, ); } if (docs.deprecated) { tagLines.push(`@deprecated ${myMarkDownToJavaDoc(docs.deprecated)}`); } // Params if ((doc as spec.Method).parameters) { const method = doc as spec.Method; if (method.parameters) { for (const param of method.parameters) { const summary = param.docs?.summary; tagLines.push(paramJavadoc(param.name, param.optional, summary)); } } } if (tagLines.length > 0) { paras.push(tagLines.join('\n')); } const lines = new Array<string>(); for (const para of paras) { if (lines.length > 0) { lines.push('<p>'); } lines.push(...para.split('\n').filter((l) => l !== '')); } this.code.line('/**'); for (const line of lines) { this.code.line(` * ${escapeEndingComment(line)}`); } this.code.line(' */'); } private getClassBase(cls: spec.ClassType) { if (!cls.base) { return 'software.amazon.jsii.JsiiObject'; } return this.toJavaType({ fqn: cls.base }); } private toDecoratedJavaType( optionalValue: spec.OptionalValue, { covariant = false } = {}, ): string { const result = this.toDecoratedJavaTypes(optionalValue, { covariant }); if (result.length > 1) { return `${ optionalValue.optional ? ANN_NULLABLE : ANN_NOT_NULL } java.lang.Object`; } return result[0]; } private toDecoratedJavaTypes( optionalValue: spec.OptionalValue, { covariant = false } = {}, ): string[] { return this.toJavaTypes(optionalValue.type, { covariant }).map( (nakedType) => `${optionalValue.optional ? ANN_NULLABLE : ANN_NOT_NULL} ${nakedType}`, ); } // Strips <*> from the type name. // necessary, because of type erasure; the compiler // will not let you check `foo instanceof Map<String, Foo>`, // and you must instead check `foo instanceof Map`. private toJavaTypeNoGenerics( type: spec.TypeReference, opts?: { forMarshalling?: boolean; covariant?: boolean }, ): string { const typeStr = this.toJavaType(type, opts); const leftAngleBracketIdx = typeStr.indexOf('<'); const rightAngleBracketIdx = typeStr.indexOf('>'); if ( (leftAngleBracketIdx < 0 && rightAngleBracketIdx >= 0) || (leftAngleBracketIdx >= 0 && rightAngleBracketIdx < 0) ) { throw new Error(`Invalid generic type: found ${typeStr}`); } return leftAngleBracketIdx > 0 && rightAngleBracketIdx > 0 ? typeStr.slice(0, leftAngleBracketIdx) : typeStr; } private toJavaType( type: spec.TypeReference, opts?: { forMarshalling?: boolean; covariant?: boolean }, ): string { const types = this.toJavaTypes(type, opts); if (types.length > 1) { return 'java.lang.Object'; } return types[0]; } private toNativeType( type: spec.TypeReference, { forMarshalling = false, covariant = false } = {}, ): string { if (spec.isCollectionTypeReference(type)) { const nativeElementType = this.toNativeType(type.collection.elementtype, { forMarshalling, covariant, }); switch (type.collection.kind) { case spec.CollectionKind.Array: return `software.amazon.jsii.NativeType.listOf(${nativeElementType})`; case spec.CollectionKind.Map: return `software.amazon.jsii.NativeType.mapOf(${nativeElementType})`; default: throw new Error( `Unsupported collection kind: ${type.collection.kind as any}`, ); } } return `software.amazon.jsii.NativeType.forClass(${this.toJavaType(type, { forMarshalling, covariant, })}.class)`; } private toJavaTypes( typeref: spec.TypeReference, { forMarshalling = false, covariant = false } = {}, ): string[] { if (spec.isPrimitiveTypeReference(typeref)) { return [this.toJavaPrimitive(typeref.primitive)]; } else if (spec.isCollectionTypeReference(typeref)) { return [this.toJavaCollection(typeref, { forMarshalling, covariant })]; } else if (spec.isNamedTypeReference(typeref)) { return [this.toNativeFqn(typeref.fqn)]; } else if (typeref.union) { const types = new Array<string>(); for (const subtype of typeref.union.types) { for (const t of this.toJavaTypes(subtype, { forMarshalling, covariant, })) { types.push(t); } } return types; } throw new Error(`Invalid type reference: ${JSON.stringify(typeref)}`); } private toJavaCollection( ref: spec.CollectionTypeReference, { forMarshalling, covariant, }: { forMarshalling: boolean; covariant: boolean }, ) { const elementJavaType = this.toJavaType(ref.collection.elementtype, { covariant, }); const typeConstraint = covariant ? makeCovariant(elementJavaType) : elementJavaType; switch (ref.collection.kind) { case spec.CollectionKind.Array: return forMarshalling ? 'java.util.List' : `java.util.List<${typeConstraint}>`; case spec.CollectionKind.Map: return forMarshalling ? 'java.util.Map' : `java.util.Map<java.lang.String, ${typeConstraint}>`; default: throw new Error( `Unsupported collection kind: ${ref.collection.kind as any}`, ); } function makeCovariant(javaType: string): string { // Don't emit a covariant expression for String (it's `final` in Java) if (javaType === 'java.lang.String') { return javaType; } return `? extends ${javaType}`; } } private toJavaPrimitive(primitive: spec.PrimitiveType) { switch (primitive) { case spec.PrimitiveType.Boolean: return 'java.lang.Boolean'; case spec.PrimitiveType.Date: return 'java.time.Instant'; case spec.PrimitiveType.Json: return 'com.fasterxml.jackson.databind.node.ObjectNode'; case spec.PrimitiveType.Number: return 'java.lang.Number'; case spec.PrimitiveType.String: return 'java.lang.String'; case spec.PrimitiveType.Any: return 'java.lang.Object'; default: throw new Error(`Unknown primitive type: ${primitive as any}`); } } private renderMethodCallArguments(method: spec.Callable) { if (!method.parameters || method.parameters.length === 0) { return ''; } const regularParams = method.parameters.filter((p) => !p.variadic); const values = regularParams.map(_renderParameter); const valueStr = `new Object[] { ${values.join(', ')} }`; if (method.variadic) { const valuesStream = `java.util.Arrays.<Object>stream(${valueStr})`; const lastParam = method.parameters[method.parameters.length - 1]; const restStream = `java.util.Arrays.<Object>stream(${lastParam.name})`; const fullStream = regularParams.length > 0 ? `java.util.stream.Stream.concat(${valuesStream}, ${restStream})` : restStream; return `, ${fullStream}.toArray(Object[]::new)`; } return `, ${valueStr}`; function _renderParameter(param: spec.Parameter) { const safeName = JavaGenerator.safeJavaPropertyName(param.name); return isNullable(param) ? safeName : `java.util.Objects.requireNonNull(${safeName}, "${safeName} is required")`; } } private renderMethodCall( cls: spec.TypeReference, method: spec.Method, async: boolean, ) { let statement = ''; if (method.static) { const javaClass = this.toJavaType(cls); statement += `software.amazon.jsii.JsiiObject.jsiiStaticCall(${javaClass}.class, `; } else { statement += `software.amazon.jsii.Kernel.${ async ? 'asyncCall' : 'call' }(this, `; } statement += `"${method.name}"`; if (method.returns) { statement += `, ${this.toNativeType(method.returns.type, { forMarshalling: true, })}`; } else { statement += ', software.amazon.jsii.NativeType.VOID'; } statement += `${this.renderMethodCallArguments(method)})`; if (method.returns) { statement = this.wrapCollection( statement, method.returns.type, method.returns.optional, ); } if (method.returns) { return `return ${statement};`; } return `${statement};`; } /** * Wraps a collection into an unmodifiable collection else returns the existing statement. * @param statement The statement to wrap if necessary. * @param type The type of the object to wrap. * @param optional Whether the value is optional (can be null/undefined) or not. * @returns The modified or original statement. */ private wrapCollection( statement: string, type: spec.TypeReference, optional?: boolean, ): string { if (spec.isCollectionTypeReference(type)) { let wrapper: string; switch (type.collection.kind) { case spec.CollectionKind.Array: wrapper = 'unmodifiableList'; break; case spec.CollectionKind.Map: wrapper = 'unmodifiableMap'; break; default: throw new Error( `Unsupported collection kind: ${type.collection.kind as any}`, ); } // In the case of "optional", the value needs ot be explicitly cast to allow for cases where the raw type was returned. return optional ? `java.util.Optional.ofNullable((${this.toJavaType( type, )})(${statement})).map(java.util.Collections::${wrapper}).orElse(null)` : `java.util.Collections.${wrapper}(${statement})`; } return statement; } private renderMethodParameters(method: spec.Callable) { const params = []; if (method.parameters) { for (const p of method.parameters) { // We can render covariant parameters only for methods that aren't overridable... so only for static methods currently. params.push( `final ${this.toDecoratedJavaType(p, { covariant: (method as spec.Method).static, })}${p.variadic ? '...' : ''} ${JavaGenerator.safeJavaPropertyName( p.name, )}`, ); } } return params.join(', '); } private renderAccessLevel(method: spec.Callable | spec.Property) { return method.protected ? 'protected' : 'public'; } private makeModuleClass(moduleName: string) { return `${this.toNativeFqn(moduleName)}.${MODULE_CLASS_NAME}`; } private emitModuleFile(mod: spec.Assembly) { const moduleName = mod.name; const moduleClass = this.makeModuleClass(moduleName); const { filePath: moduleResFile, name: moduleResName } = this.toJavaResourcePath(mod, `${mod.name}.${MODULE_CLASS_NAME}`); this.code.openFile(moduleResFile); for (const fqn of Object.keys(this.assembly.types ?? {})) { this.code.line(`${fqn}=${this.toNativeFqn(fqn, { binaryName: true })}`); } this.code.closeFile(moduleResFile); const moduleFile = this.toJavaFilePath(mod, { assembly: mod.name, fqn: `${mod.name}.${MODULE_CLASS_NAME}`, kind: spec.TypeKind.Class, name: MODULE_CLASS_NAME, }); this.code.openFile(moduleFile); this.code.line(`package ${this.toNativeName(mod).packageName};`); this.code.line(); if (Object.keys(mod.dependencies ?? {}).length > 0) { this.code.line('import static java.util.Arrays.asList;'); this.code.line(); } this.code.line('import java.io.BufferedReader;'); this.code.line('import java.io.InputStream;'); this.code.line('import java.io.InputStreamReader;'); this.code.line('import java.io.IOException;'); this.code.line('import java.io.Reader;'); this.code.line('import java.io.UncheckedIOException;'); this.code.line(); this.code.line('import java.nio.charset.StandardCharsets;'); this.code.line(); this.code.line('import java.util.HashMap;'); if (Object.keys(mod.dependencies ?? {}).length > 0) { this.code.line('import java.util.List;'); } this.code.line('import java.util.Map;'); this.code.line(); this.code.line('import software.amazon.jsii.JsiiModule;'); this.code.line(); this.code.line(ANN_INTERNAL); this.code.openBlock( `public final class ${MODULE_CLASS_NAME} extends JsiiModule`, ); this.code.line( 'private static final Map<String, String> MODULE_TYPES = load();', ); this.code.line(); this.code.openBlock('private static Map<String, String> load()'); this.code.line('final Map<String, String> result = new HashMap<>();'); this.code.line( `final ClassLoader cl = ${MODULE_CLASS_NAME}.class.getClassLoader();`, ); this.code.line( `try (final InputStream is = cl.getResourceAsStream("${moduleResName}");`, ); this.code.line( ' final Reader rd = new InputStreamReader(is, StandardCharsets.UTF_8);', ); this.code.openBlock( ' final BufferedReader br = new BufferedReader(rd))', ); this.code.line('br.lines()'); this.code.line(' .filter(line -> !line.trim().isEmpty())'); this.code.openBlock(' .forEach(line -> '); this.code.line('final String[] parts = line.split("=", 2);'); this.code.line('final String fqn = parts[0];'); this.code.line('final String className = parts[1];'); this.code.line('result.put(fqn, className);'); this.code.unindent('});'); // Proxy for closeBlock this.code.closeBlock(); this.code.openBlock('catch (final IOException exception)'); this.code.line('throw new UncheckedIOException(exception);'); this.code.closeBlock(); this.code.line('return result;'); this.code.closeBlock(); this.code.line(); this.code.line( 'private final Map<String, Class<?>> cache = new HashMap<>();', ); this.code.line(); // ctor this.code.openBlock(`public ${MODULE_CLASS_NAME}()`); this.code.line( `super("${moduleName}", "${ mod.version }", ${MODULE_CLASS_NAME}.class, "${this.getAssemblyFileName()}");`, ); this.code.closeBlock(); // ctor // dependencies if (mod.dependencies && Object.keys(mod.dependencies).length > 0) { const deps = []; for (const dep of Object.keys(mod.dependencies)) { deps.push(`${this.makeModuleClass(dep)}.class`); } this.code.line(); this.code.line('@Override'); this.code.openBlock( 'public List<Class<? extends JsiiModule>> getDependencies()', ); this.code.line(`return asList(${deps.join(', ')});`); this.code.closeBlock(); } this.code.line(); this.code.line('@Override'); this.code.openBlock( 'protected Class<?> resolveClass(final String fqn) throws ClassNotFoundException', ); this.code.openBlock('if (!MODULE_TYPES.containsKey(fqn))'); this.code.line( 'throw new ClassNotFoundException("Unknown JSII type: " + fqn);', ); this.code.closeBlock(); this.code.line('String className = MODULE_TYPES.get(fqn);'); this.code.openBlock('if (!this.cache.containsKey(className))'); this.code.line('this.cache.put(className, this.findClass(className));'); this.code.closeBlock(); this.code.line('return this.cache.get(className);'); this.code.closeBlock(); this.code.line(); this.code.openBlock('private Class<?> findClass(final String binaryName)'); this.code.openBlock('try'); this.code.line('return Class.forName(binaryName);'); this.code.closeBlock(); this.code.openBlock('catch (final ClassNotFoundException exception)'); this.code.line('throw new RuntimeException(exception);'); this.code.closeBlock(); this.code.closeBlock(); this.code.closeBlock(); this.code.closeFile(moduleFile); return moduleClass; } private emitJsiiInitializers(cls: spec.ClassType) { this.code.line(); this.code.openBlock( `protected ${cls.name}(final software.amazon.jsii.JsiiObjectRef objRef)`, ); this.code.line('super(objRef);'); this.code.closeBlock(); this.code.line(); this.code.openBlock( `protected ${cls.name}(final software.amazon.jsii.JsiiObject.InitializationMode initializationMode)`, ); this.code.line('super(initializationMode);'); this.code.closeBlock(); } /** * Computes the java FQN for a JSII FQN: * 1. Determine which assembly the FQN belongs to (first component of the FQN) * 2. Locate the `targets.java.package` value for that assembly (this assembly, or one of the dependencies) * 3. Return the java FQN: ``<module.targets.java.package>.<FQN stipped of first component>`` * * Records an assembly reference if the referenced FQN comes from a different assembly. * * @param fqn the JSII FQN to be used. * * @returns the corresponding Java FQN. * * @throws if the assembly the FQN belongs to does not have a `targets.java.package` set. */ private toNativeFqn( fqn: string, { binaryName }: { binaryName: boolean } = { binaryName: false }, ): string { const [mod] = fqn.split('.'); const depMod = this.findModule(mod); // Make sure any dependency (direct or transitive) of which any type is explicitly referenced by the generated // code is included in the generated POM's dependencies section (protecting the artifact from changes in the // dependencies' dependency structure). if (mod !== this.assembly.name) { this.referencedModules[mod] = depMod; const translated = translateFqn({ ...depMod, name: mod }, fqn); return [translated.packageName, ...translated.typeNames].join('.'); } const { packageName, typeName } = fqn === this.assembly.name ? this.toNativeName(this.assembly) : this.toNativeName(this.assembly, this.assembly.types![fqn]); const className = typeName && binaryName ? typeName.replace('.', '$') : typeName; return `${packageName}${className ? `.${className}` : ''}`; } /** * Computes Java name for a jsii assembly or type. * * @param assm The assembly that contains the type * @param type The type we want the name of */ private toNativeName(assm: spec.Assembly): { packageName: string; typeName: undefined; }; private toNativeName( assm: spec.Assembly, type: spec.Type, ): { packageName: string; typeName: string }; private toNativeName( assm: spec.Assembly, type?: spec.Type, ): { packageName: string; typeName?: string } { if (!type) { return { packageName: packageNameFromAssembly(assm) }; } const translated = translateFqn(assm, type.fqn); return { packageName: translated.packageName, typeName: translated.typeNames.join('.'), }; } /** * Emits an ``@Generated`` annotation honoring the ``this.emitFullGeneratorInfo`` setting. */ private emitGeneratedAnnotation() { const date = this.emitFullGeneratorInfo ? `, date = "${new Date().toISOString()}"` : ''; const generator = this.emitFullGeneratorInfo ? `jsii-pacmak/${VERSION_DESC}` : 'jsii-pacmak'; this.code.line( `@javax.annotation.Generated(value = "${generator}"${date})`, ); } private convertExample(example: string, api: ApiLocation): string { const translated = this.rosetta.translateExample( api, example, TargetLanguage.JAVA, enforcesStrictMode(this.assembly), ); return translated.source; } private convertSamplesInMarkdown(markdown: string, api: ApiLocation): string { return this.rosetta.translateSnippetsInMarkdown( api, markdown, TargetLanguage.JAVA, enforcesStrictMode(this.assembly), ); } /** * Fins the Java FQN of the default implementation interfaces that should be implemented when a new * default interface or proxy class is being emitted. * * @param type the type for which a default interface or proxy is emitted. * @param includeThisType whether this class's default interface should be included or not. * * @returns a list of Java fully qualified class names. */ private defaultInterfacesFor( type: reflect.ClassType | reflect.InterfaceType, { includeThisType = false }: { includeThisType?: boolean } = {}, ): string[] { const result = new Set<string>(); if ( includeThisType && hasDefaultInterfaces(type.assembly) && type.isInterfaceType() ) { result.add( `${this.toNativeFqn(type.fqn)}.${INTERFACE_DEFAULT_CLASS_NAME}`, ); } else { for (const iface of type.interfaces) { if (hasDefaultInterfaces(iface.assembly)) { result.add( `${this.toNativeFqn(iface.fqn)}.${INTERFACE_DEFAULT_CLASS_NAME}`, ); } else { for (const item of this.defaultInterfacesFor(iface)) { result.add(item); } } } if (type.isClassType() && type.base != null) { for (const item of this.defaultInterfacesFor(type.base)) { result.add(item); } } } return Array.from(result); } } /** * This models the POM schema for a <dependency> entity * @see https://maven.apache.org/pom.html#Dependencies */ interface MavenDependency { groupId: string; artifactId: string; version: string; type?: string; scope?: 'compile' | 'provided' | 'runtime' | 'test' | 'system'; systemPath?: string; optional?: boolean; '#comment'?: string; } /** * Looks up the `@jsii/java-runtime` package from the local repository. * If it contains a "maven-repo" directory, it will be added as a local maven repo * so when we build locally, we build against it and not against the one published * to Maven Central. */ function findJavaRuntimeLocalRepository() { try { // eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports,import/no-extraneous-dependencies const javaRuntime = require('@jsii/java-runtime'); logging.info( `Using local version of the Java jsii runtime package at: ${javaRuntime.repository}`, ); return javaRuntime.repository; } catch { return undefined; } } function isNullable(optionalValue: spec.OptionalValue | undefined): boolean { if (!optionalValue) { return false; } return ( optionalValue.optional || (spec.isPrimitiveTypeReference(optionalValue.type) && optionalValue.type.primitive === spec.PrimitiveType.Any) ); } function paramJavadoc( name: string, optional?: boolean, summary?: string, ): string { const parts = ['@param', name]; if (summary) { parts.push(stripNewLines(myMarkDownToJavaDoc(endWithPeriod(summary)))); } if (!optional) { parts.push('This parameter is required.'); } return parts.join(' '); } function endWithPeriod(s: string): string { s = s.trimRight(); if (!s.endsWith('.')) { return `${s}.`; } return s; } function computeOverrides<T extends { param: spec.Parameter }>( allParams: T[], ): Iterable<T[]> { return { [Symbol.iterator]: function* () { yield allParams; while (allParams.length > 0) { const lastParam = allParams[allParams.length - 1]; if (!lastParam.param.variadic && !lastParam.param.optional) { // Neither variadic nor optional -- we're done here! return; } allParams = allParams.slice(0, allParams.length - 1); // If the lastParam was variadic, we shouldn't generate an override without it only. if (lastParam.param.optional) { yield allParams; } } }, }; } type AssemblyLike = spec.AssemblyConfiguration & { name: string }; /** * Return the native package name from an assembly */ function packageNameFromAssembly(assm: AssemblyLike) { const javaPackage = assm.targets?.java?.package; if (!javaPackage) { throw new Error( `The module ${assm.name} does not have a java.package setting`, ); } return javaPackage; } /** * Analyzes and translates a jsii FQN to Java components * * The FQN can be of the assembly, a submodule, or a type. * * Any type can have the following characteristics: * * - Located in zero or more nested submodules. Any of these can have a Java * package name assigned--if none, the name is automatically determined by * snake casing. At least the assembly must have a Java package name assigned. * - Located in zero or more nested types (classes). * * Find up the set of namespaces until we find a submodule (or the assembly * itself) that has an explicit Java package name defined. * * Append all the namespaces that we crossed that didn't have a package name defined. * * Returns the Java package name determined this way, as well as the hierarchy of type * names inside the package. `typeNames` may be missing or empty if the FQN indicated * the assembly or a submodule, contains 1 element if the FQN indicated a top-level type, * multiple elements if the FQN indicated a nested type. */ function translateFqn( assm: AssemblyLike, originalFqn: string, ): { packageName: string; typeNames: string[] } { const implicitPackageNames = new Array<string>(); const typeNames = new Array<string>(); // We work ourselves upward through the FQN until we've found an explicit package let packageName = packageNameFromAssembly(assm); let fqn = originalFqn; while (fqn !== '' && fqn !== assm.name) { const [parentFqn, lastPart] = splitNamespace(fqn); const submodule = assm.submodules?.[fqn]; if (submodule) { const explicitPackage = submodule.targets?.java?.package; if (explicitPackage) { packageName = explicitPackage; // We can stop recursing, types cannot be the parent of a module and nothing upwards can change // the package name anymore break; } implicitPackageNames.unshift(`.${toSnakeCase(lastPart)}`); } else { // If it's not a submodule, it must be a type. typeNames.unshift(lastPart); } fqn = parentFqn; } if (fqn === '') { throw new Error(`Could not find '${originalFqn}' inside '${assm.name}'`); } return { packageName: `${packageName}${implicitPackageNames.join('')}`, typeNames, }; } /** * Determines whether the provided assembly exposes the "hasDefaultInterfaces" * jsii-pacmak feature flag. * * @param assembly the assembly that is tested * * @returns true if the Jsii$Default interfaces can be used */ function hasDefaultInterfaces(assembly: reflect.Assembly): boolean { return !!assembly.metadata?.jsii?.pacmak?.hasDefaultInterfaces; } /** * Whecks whether a name corresponds to a method on `java.lang.Object`. It is not * possible to emit default interface implementation for those names because * these would always be replaced by the implementations on `Object`. * * @param name the checked name * * @returns `true` if a default implementation cannot be generated for this name. */ function isJavaLangObjectMethodName(name: string): boolean { return JAVA_LANG_OBJECT_METHOD_NAMES.has(name); } const JAVA_LANG_OBJECT_METHOD_NAMES = new Set([ 'clone', 'equals', 'finalize', 'getClass', 'hashCode', 'notify', 'notifyAll', 'toString', 'wait', ]); /** * In a dotted string, strip off the last dotted component */ function splitNamespace(ns: string): [string, string] { const dot = ns.lastIndexOf('.'); if (dot === -1) { return ['', ns]; } return [ns.slice(0, dot), ns.slice(dot + 1)]; } /** * Escape a string for dropping into JavaDoc */ function escape(s: string) { return s.replace(/["\\<>&]/g, (c) => `&#${c.charCodeAt(0)};`); } function containsUnionType( typeRef: spec.TypeReference, ): typeRef is spec.UnionTypeReference | spec.CollectionTypeReference { return ( spec.isUnionTypeReference(typeRef) || (spec.isCollectionTypeReference(typeRef) && containsUnionType(typeRef.collection.elementtype)) ); } function myMarkDownToJavaDoc(source: string) { if (source.includes('{@link') || source.includes('{@code')) { // Slightly dirty hack: if we are seeing this, it means the docstring was provided literally // in this file. These strings are safe to not be escaped, and in fact escaping them will // break them: they will turn into `{&#64;`, which breaks the JavaDoc markup. // // Since docstring do not (or at least should not) contain JavaDoc literals, this is safe. return source; } return markDownToJavaDoc(source); } function stripNewLines(x: string) { return x.replace(/\n/g, ''); } // Replace */ with *\/ to avoid closing the comment block function escapeEndingComment(x: string) { return x.replace(/\*\//g, '*\\/'); }