jsii-pacmak
Version:
A code generation framework for jsii backend languages
2,979 lines • 133 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.JavaBuilder = void 0;
const spec = __importStar(require("@jsii/spec"));
const assert_1 = __importDefault(require("assert"));
const clone_1 = __importDefault(require("clone"));
const case_utils_1 = require("codemaker/lib/case-utils");
const crypto_1 = require("crypto");
const fs = __importStar(require("fs-extra"));
const reflect = __importStar(require("jsii-reflect"));
const jsii_rosetta_1 = require("jsii-rosetta");
const path = __importStar(require("path"));
const xmlbuilder = __importStar(require("xmlbuilder"));
const generator_1 = require("../generator");
const logging = __importStar(require("../logging"));
const naming_util_1 = require("../naming-util");
const target_1 = require("../target");
const util_1 = require("../util");
const version_1 = require("../version");
const _utils_1 = require("./_utils");
const version_utils_1 = require("./version-utils");
const rosetta_assembly_1 = require("../rosetta-assembly");
const type_utils_1 = require("../type-utils");
const type_visitor_1 = require("../type-visitor");
const type_literals_1 = require("./type-literals");
const index_1 = require("./index");
// eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports
const spdxLicenseList = require('spdx-license-list');
const BUILDER_CLASS_NAME = 'Builder';
const ANN_NOT_NULL = '@org.jetbrains.annotations.NotNull';
const ANN_NULLABLE = '@org.jetbrains.annotations.Nullable';
const ANN_INTERNAL = '@software.amazon.jsii.Internal';
/**
* Because of a historical bug, $Default interfaces we inherit from might not
* have all method overloads generated correctly.
*
* So when inheriting these, we might need to generate the overloads in
* subinterfaces/subclasses.
*/
const GENERATE_POTENTIALLY_MISING_DEFAULT_OVERLOADS = true;
/**
* Build Java packages all together, by generating an aggregate POM
*
* This will make the Java build a lot more efficient (~300%).
*
* Do this by copying the code into a temporary directory, generating an aggregate
* POM there, and then copying the artifacts back into the respective output
* directories.
*/
class JavaBuilder {
constructor(modules, options) {
this.modules = modules;
this.options = options;
this.targetName = 'java';
}
async buildModules() {
if (this.modules.length === 0) {
return;
}
if (this.options.codeOnly) {
// Simple, just generate code to respective output dirs
await Promise.all(this.modules.map((module) => this.generateModuleCode(module, this.options, this.outputDir(module.outputDirectory))));
return;
}
// Otherwise make a single tempdir to hold all sources, build them together and copy them back out
const scratchDirs = [];
try {
const tempSourceDir = await this.generateAggregateSourceDir(this.modules, this.options);
scratchDirs.push(tempSourceDir);
// Need any old module object to make a target to be able to invoke build, though none of its settings
// will be used.
const target = this.makeTarget(this.modules[0], this.options);
await target.resolveMavenVersions(tempSourceDir.directory);
const tempOutputDir = await util_1.Scratch.make(async (dir) => {
logging.debug(`Building Java code to ${dir}`);
await target.build(tempSourceDir.directory, dir);
});
scratchDirs.push(tempOutputDir);
await this.copyOutArtifacts(tempOutputDir.directory, tempSourceDir.object);
if (this.options.clean) {
await util_1.Scratch.cleanupAll(scratchDirs);
}
}
catch (e) {
logging.warn(`Exception occurred, not cleaning up ${scratchDirs
.map((s) => s.directory)
.join(', ')}`);
throw e;
}
}
async generateModuleCode(module, options, where) {
const target = this.makeTarget(module, options);
logging.debug(`Generating Java code into ${where}`);
await target.generateCode(where, module.tarball);
}
async generateAggregateSourceDir(modules, options) {
return util_1.Scratch.make(async (tmpDir) => {
logging.debug(`Generating aggregate Java source dir at ${tmpDir}`);
const ret = [];
const generatedModules = modules
.map((module) => ({ module, relativeName: (0, util_1.slugify)(module.name) }))
.map(({ module, relativeName }) => ({
module,
relativeName,
sourceDir: path.join(tmpDir, relativeName),
}))
.map(({ module, relativeName, sourceDir }) => this.generateModuleCode(module, options, sourceDir).then(() => ({
module,
relativeName,
})));
for (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;
});
}
async generateAggregatePom(where, moduleNames) {
const aggregatePom = xmlbuilder
.create({
project: {
'@xmlns': 'http://maven.apache.org/POM/4.0.0',
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'@xsi:schemaLocation': 'http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd',
'#comment': [
`Generated by jsii-pacmak@${version_1.VERSION_DESC} on ${new Date().toISOString()}`,
],
modelVersion: '4.0.0',
packaging: 'pom',
groupId: 'software.amazon.jsii',
artifactId: 'aggregatepom',
version: '1.0.0',
modules: {
module: moduleNames,
},
},
}, { encoding: 'UTF-8' })
.end({ pretty: true });
logging.debug(`Generated ${where}/pom.xml`);
await fs.writeFile(path.join(where, 'pom.xml'), aggregatePom);
}
async copyOutArtifacts(artifactsRoot, packages) {
logging.debug('Copying out Java artifacts');
// The artifacts directory looks like this:
// /tmp/XXX/software/amazon/awscdk/something/v1.2.3
// /else/v1.2.3
// /entirely/v1.2.3
//
// We get the 'software/amazon/awscdk/something' path from the package, identifying
// the files we need to copy, including Maven metadata. But we need to recreate
// the whole path in the target directory.
await Promise.all(packages.map(async (pkg) => {
const artifactsSource = path.join(artifactsRoot, pkg.relativeArtifactsDir);
const artifactsDest = path.join(this.outputDir(pkg.outputTargetDirectory), pkg.relativeArtifactsDir);
await fs.mkdirp(artifactsDest);
await fs.copy(artifactsSource, artifactsDest, { recursive: true });
}));
}
/**
* Decide whether or not to append 'java' to the given output directory
*/
outputDir(declaredDir) {
return this.options.languageSubdirectory
? path.join(declaredDir, this.targetName)
: declaredDir;
}
/**
* Generates maven settings file for this build.
* @param where The generated sources directory. This is where user.xml will be placed.
* @param currentOutputDirectory The current output directory. Will be added as a local maven repo.
*/
async generateMavenSettingsForLocalDeps(where) {
const filePath = path.join(where, 'user.xml');
// traverse the dep graph of this module and find all modules that have
// an <outdir>/java directory. we will add those as local maven
// repositories which will resolve instead of Maven Central for those
// module. this enables building against local modules (i.e. in lerna
// repositories or linked modules).
const allDepsOutputDirs = new Set();
const resolvedModules = this.modules.map(async (mod) => ({
module: mod,
localBuildDirs: await (0, target_1.findLocalBuildDirs)(mod.moduleDirectory, this.targetName),
}));
for (const { module, localBuildDirs } of await Promise.all(resolvedModules)) {
(0, util_1.setExtend)(allDepsOutputDirs, localBuildDirs);
// Also include output directory where we're building to, in case we build multiple packages into
// the same output directory.
allDepsOutputDirs.add(path.join(module.outputDirectory, this.options.languageSubdirectory ? this.targetName : ''));
}
const localRepos = Array.from(allDepsOutputDirs);
// if java-runtime is checked-out and we can find a local repository,
// add it to the list.
const localJavaRuntime = await findJavaRuntimeLocalRepository();
if (localJavaRuntime) {
localRepos.push(localJavaRuntime);
}
logging.debug('local maven repos:', localRepos);
const profileName = 'local-jsii-modules';
const localRepository = this.options.arguments['maven-local-repository'];
const settings = xmlbuilder
.create({
settings: {
'@xmlns': 'http://maven.apache.org/POM/4.0.0',
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'@xsi:schemaLocation': 'http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd',
'#comment': [
`Generated by jsii-pacmak@${version_1.VERSION_DESC} on ${new Date().toISOString()}`,
],
// Do *not* attempt to ask the user for stuff...
interactiveMode: false,
// Use a non-default local repository (unless java-custom-cache-path arg is provided) to isolate from cached artifacts...
localRepository: localRepository
? path.resolve(process.cwd(), localRepository)
: path.resolve(where, '.m2', 'repository'),
// Register locations of locally-sourced dependencies
profiles: {
profile: {
id: profileName,
repositories: {
repository: localRepos.map((repo) => ({
id: repo.replace(/[\\/:"<>|?*]/g, '$'),
url: `file://${repo}`,
})),
},
},
},
activeProfiles: {
activeProfile: profileName,
},
},
}, { encoding: 'UTF-8' })
.end({ pretty: true });
logging.debug(`Generated ${filePath}`);
await fs.writeFile(filePath, settings);
return filePath;
}
makeTarget(module, options) {
return new Java({
arguments: options.arguments,
assembly: module.assembly,
fingerprint: options.fingerprint,
force: options.force,
packageDir: module.moduleDirectory,
rosetta: options.rosetta,
runtimeTypeChecking: options.runtimeTypeChecking,
targetName: this.targetName,
});
}
}
exports.JavaBuilder = JavaBuilder;
/**
* Return the subdirectory of the output directory where the artifacts for this particular package are produced
*/
function moduleArtifactsSubdir(module) {
const groupId = module.assembly.targets.java.maven.groupId;
const artifactId = module.assembly.targets.java.maven.artifactId;
return `${groupId.replace(/\./g, '/')}/${artifactId}`;
}
class Java extends target_1.Target {
static toPackageInfos(assm) {
const groupId = assm.targets.java.maven.groupId;
const artifactId = assm.targets.java.maven.artifactId;
const releaseVersion = (0, version_utils_1.toReleaseVersion)(assm.version, index_1.TargetName.JAVA);
const url = `https://repo1.maven.org/maven2/${groupId.replace(/\./g, '/')}/${artifactId}/${assm.version}/`;
return {
java: {
repository: 'Maven Central',
url,
usage: {
'Apache Maven': {
language: 'xml',
code: xmlbuilder
.create({
dependency: { groupId, artifactId, version: releaseVersion },
})
.end({ pretty: true })
.replace(/<\?\s*xml(\s[^>]+)?>\s*/m, ''),
},
'Apache Buildr': `'${groupId}:${artifactId}:jar:${releaseVersion}'`,
'Apache Ivy': {
language: 'xml',
code: xmlbuilder
.create({
dependency: {
'@groupId': groupId,
'@name': artifactId,
'@rev': releaseVersion,
},
})
.end({ pretty: true })
.replace(/<\?\s*xml(\s[^>]+)?>\s*/m, ''),
},
'Groovy Grape': `@Grapes(\n@Grab(group='${groupId}', module='${artifactId}', version='${releaseVersion}')\n)`,
'Gradle / Grails': `compile '${groupId}:${artifactId}:${releaseVersion}'`,
},
},
};
}
static toNativeReference(type, options) {
const [, ...name] = type.fqn.split('.');
return { java: `import ${[options.package, ...name].join('.')};` };
}
constructor(options) {
super(options);
this.generator = new JavaGenerator(options);
}
async build(sourceDir, outDir) {
const url = `file://${outDir}`;
await this.invokeMaven(sourceDir, ['deploy', `-D=altDeploymentRepository=local::default::${url}`], {
retry: { maxAttempts: 5 },
});
}
/**
* Run the maven 'versions:resolve-ranges' plugin
*
* Initially, we generate version ranges into the pom file based on the NPM
* version ranges.
*
* At build time, given a dependency version range, Maven will download metadata
* for all possible versions before every (uncached) build. This takes a long
* time, before finally resolving to the latest version anyway.
*
* Instead, we use the Maven 'versions' plugin to resolve our wide ranges to
* point versions. We want the "latest matching" version anyway, and if we don't
* the resolution now (which downloads the .poms of all possible versions) it
* will happen during every single build.
*/
async resolveMavenVersions(directory) {
const versionsPluginVersion = '2.20.1';
await this.invokeMaven(directory, [
`org.codehaus.mojo:versions-maven-plugin:${versionsPluginVersion}:resolve-ranges`,
], {
retry: { maxAttempts: 1 },
});
}
async invokeMaven(directory, args, options) {
// Pass through jsii-pacmak --mvn-xyz=... arguments as --xyz=...
const passThruArgs = new Array();
for (const arg of Object.keys(this.arguments)) {
if (!arg.startsWith('mvn-')) {
continue;
}
passThruArgs.push(`--${arg.slice(4)}`);
passThruArgs.push(this.arguments[arg].toString());
}
await (0, util_1.subprocess)('mvn', [
// If we don't run in verbose mode, turn on quiet mode
...(this.arguments.verbose ? [] : ['--quiet']),
'--batch-mode',
...args,
...passThruArgs,
'--settings=user.xml',
], {
cwd: directory,
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`,
...options?.env,
},
...options,
});
}
}
exports.default = Java;
// ##################
// # CODE GENERATOR #
// ##################
const MODULE_CLASS_NAME = '$Module';
const INTERFACE_PROXY_CLASS_NAME = 'Jsii$Proxy';
const INTERFACE_DEFAULT_CLASS_NAME = 'Jsii$Default';
class JavaGenerator extends generator_1.Generator {
/**
* Turns a raw javascript property name (eg: 'default') into a safe Java property name (eg: 'defaultValue').
* @param propertyName the raw JSII property Name
*/
static safeJavaPropertyName(propertyName) {
if (!propertyName) {
return propertyName;
}
if (propertyName === '_') {
// Slightly different pattern for this one. We used to generate `__` here
// but it's somewhat likely that people will use `_, __, ___` as multiple
// indifferent arguments, so we pick a different name.
//
// Ideally we would look at the alternative argument names and pick
// something guaranteed to be unique, but unfortunately the code isn't
// quite structured that way so we'll pick something unlikely to collide
// instead.
//
// Changing from `__` -> `_under` would be a breaking change if applied to
// public property names, but most likely this will be used for function
// parameters (unfortunately the code has been structured in such a way
// that property and parameter names are strongly tied together, in a way
// that would take more time to unwind than I care to invest right now),
// where it doesn't matter.
return '_under_';
}
if (JavaGenerator.RESERVED_KEYWORDS.includes(propertyName)) {
return `${propertyName}Value`;
}
return propertyName;
}
/**
* Turns a raw javascript method name (eg: 'import') into a safe Java method name (eg: 'doImport').
* @param methodName
*/
static safeJavaMethodName(methodName) {
if (!methodName) {
return methodName;
}
if (methodName === '_') {
// Different pattern for this one. Also this should never happen, who names a function '_' ??
return 'doIt';
}
if (JavaGenerator.RESERVED_KEYWORDS.includes(methodName)) {
return `do${(0, naming_util_1.jsiiToPascalCase)(methodName)}`;
}
return methodName;
}
constructor(options) {
super({ ...options, generateOverloadsForMethodWithOptionals: true });
/**
* A map of all the modules ever referenced during code generation. These include
* direct dependencies but can potentially also include transitive dependencies, when,
* for example, we need to refer to their types when flatting the class hierarchy for
* interface proxies.
*/
this.referencedModules = {};
this.rosetta = options.rosetta;
}
onBeginAssembly(assm, fingerprint) {
this.emitFullGeneratorInfo = fingerprint;
this.moduleClass = this.emitModuleFile(assm);
this.emitAssemblyPackageInfo(assm);
}
onEndAssembly(assm, fingerprint) {
this.emitMavenPom(assm, fingerprint);
delete this.emitFullGeneratorInfo;
}
getAssemblyOutputDir(mod) {
const dir = this.toNativeFqn(mod.name).replace(/\./g, '/');
return path.join('src', 'main', 'resources', dir);
}
onBeginClass(cls, abstract) {
this.openFileIfNeeded(cls);
this.addJavaDocs(cls, { api: 'type', fqn: cls.fqn });
const classBase = this.getClassBase(cls);
const extendsExpression = classBase
? ` extends ${displayStatic(classBase)}`
: '';
let implementsExpr = '';
if (cls.interfaces?.length ?? 0 > 0) {
implementsExpr = ` implements ${cls
.interfaces.map((x) => this.toNativeFqn(x))
.join(', ')}`;
}
const nested = this.isNested(cls);
const inner = nested ? ' static' : '';
const absPrefix = abstract ? ' abstract' : '';
if (!nested) {
this.emitGeneratedAnnotation();
}
this.emitStabilityAnnotations(cls);
this.code.line(`@software.amazon.jsii.Jsii(module = ${this.moduleClass}.class, fqn = "${cls.fqn}")`);
this.code.openBlock(`public${inner}${absPrefix} class ${cls.name}${extendsExpression}${implementsExpr}`);
this.emitJsiiInitializers(cls);
this.emitStaticInitializer(cls);
}
onEndClass(cls) {
if (cls.abstract) {
const type = this.reflectAssembly.findType(cls.fqn);
this.emitProxy(type);
}
else {
this.emitClassBuilder(cls);
}
this.code.closeBlock();
this.closeFileIfNeeded(cls);
}
onInitializer(cls, method) {
this.code.line();
// If needed, patching up the documentation to point users at the builder pattern
this.addJavaDocs(method, { api: 'initializer', fqn: cls.fqn });
this.emitStabilityAnnotations(method);
// Abstract classes should have protected initializers
const initializerAccessLevel = cls.abstract
? 'protected'
: this.renderAccessLevel(method);
const types = this.convertTypes(method.parameters);
if (types.some((t) => t.type === 'param')) {
throw new Error('Cannot have generic type arguments to a constructor');
}
// NOTE: even though a constructor is technically final and we COULD render covariant types, historically we didn't and I'm not changing it.
this.code.openBlock(`${initializerAccessLevel} ${cls.name}(${this.renderParameters(method.parameters, types, 'exact-types')})`);
this.code.line('super(software.amazon.jsii.JsiiObject.InitializationMode.JSII);');
this.emitUnionParameterValidation(method.parameters, 'exact-types');
this.code.line(`software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this${this.renderMethodCallArguments(method)});`);
this.code.closeBlock();
}
onInitializerOverload(cls, overload, _originalInitializer) {
this.onInitializer(cls, overload);
}
onField(_cls, _prop, _union) {
/* noop */
}
onProperty(cls, prop) {
this.emitProperty(cls, prop, cls);
}
onStaticProperty(cls, prop) {
if (prop.const) {
this.emitConstProperty(cls, prop);
}
else {
this.emitProperty(cls, prop, cls);
}
}
/**
* Since we expand the union setters, we will use this event to only emit the getter which returns an Object.
*/
onUnionProperty(cls, prop, _union) {
this.emitProperty(cls, prop, cls);
}
onMethod(cls, method) {
this.emitMethod(cls, method);
}
onMethodOverload(cls, overload, _originalMethod) {
this.onMethod(cls, overload);
}
onStaticMethod(cls, method) {
this.emitMethod(cls, method);
}
onStaticMethodOverload(cls, overload, _originalMethod) {
this.emitMethod(cls, overload);
}
onBeginEnum(enm) {
this.openFileIfNeeded(enm);
this.addJavaDocs(enm, { api: 'type', fqn: enm.fqn });
if (!this.isNested(enm)) {
this.emitGeneratedAnnotation();
}
this.emitStabilityAnnotations(enm);
this.code.line(`@software.amazon.jsii.Jsii(module = ${this.moduleClass}.class, fqn = "${enm.fqn}")`);
this.code.openBlock(`public enum ${enm.name}`);
}
onEndEnum(enm) {
this.code.closeBlock();
this.closeFileIfNeeded(enm);
}
onEnumMember(parentType, member) {
this.addJavaDocs(member, {
api: 'member',
fqn: parentType.fqn,
memberName: member.name,
});
this.emitStabilityAnnotations(member);
this.code.line(`${member.name},`);
}
/**
* Namespaces are handled implicitly by onBeginClass().
*
* Only emit package-info in case this is a submodule
*/
onBeginNamespace(ns) {
const submodule = this.assembly.submodules?.[ns];
if (submodule) {
this.emitSubmodulePackageInfo(this.assembly, ns);
}
}
onEndNamespace(_ns) {
/* noop */
}
onBeginInterface(ifc) {
this.openFileIfNeeded(ifc);
this.addJavaDocs(ifc, { api: 'type', fqn: ifc.fqn });
// all interfaces always extend JsiiInterface so we can identify that it is a jsii interface.
const interfaces = ifc.interfaces ?? [];
const bases = [
'software.amazon.jsii.JsiiSerializable',
...interfaces.map((x) => this.toNativeFqn(x)),
].join(', ');
const nested = this.isNested(ifc);
const inner = nested ? ' static' : '';
if (!nested) {
this.emitGeneratedAnnotation();
}
this.code.line(`@software.amazon.jsii.Jsii(module = ${this.moduleClass}.class, fqn = "${ifc.fqn}")`);
this.code.line(`@software.amazon.jsii.Jsii.Proxy(${ifc.name}.${INTERFACE_PROXY_CLASS_NAME}.class)`);
this.emitStabilityAnnotations(ifc);
this.code.openBlock(`public${inner} interface ${ifc.name} extends ${bases}`);
}
onEndInterface(ifc) {
this.emitMultiplyInheritedProperties(ifc);
if (ifc.datatype) {
this.emitDataType(ifc);
}
else {
const type = this.reflectAssembly.findType(ifc.fqn);
this.emitProxy(type);
// We don't emit Jsii$Default if the assembly opted out of it explicitly.
// This is mostly to facilitate compatibility testing...
if (hasDefaultInterfaces(this.reflectAssembly)) {
this.emitDefaultImplementation(type);
}
}
this.code.closeBlock();
this.closeFileIfNeeded(ifc);
}
onInterfaceMethod(ifc, method) {
this.code.line();
const returnType = method.returns
? forceSingleType(this.toDecoratedJavaTypes(method.returns))
: mkStatic('void');
const methodName = JavaGenerator.safeJavaMethodName(method.name);
this.addJavaDocs(method, {
api: 'member',
fqn: ifc.fqn,
memberName: methodName,
}, {
returnsUnion: method.returns?.type,
});
this.emitStabilityAnnotations(method);
const types = this.convertTypes(method.parameters);
this.code.line(`${typeVarDeclarations(types)}${displayStatic(returnType)} ${methodName}(${this.renderParameters(method.parameters, types, 'exact-types')});`);
}
onInterfaceMethodOverload(ifc, overload, _originalMethod) {
this.onInterfaceMethod(ifc, overload);
}
onInterfaceProperty(ifc, prop) {
let apparentGetterType = prop;
// We can only ever return one type, so take the first one
if (spec.isIntersectionTypeReference(prop.type)) {
apparentGetterType = {
optional: prop.optional,
type: prop.type.intersection.types[0],
};
}
const getterType = forceSingleType(this.toDecoratedJavaTypes(apparentGetterType));
const propName = (0, naming_util_1.jsiiToPascalCase)(JavaGenerator.safeJavaPropertyName(prop.name));
// for unions we only generate overloads for setters, not getters.
this.code.line();
this.addJavaDocs(prop, {
api: 'member',
fqn: ifc.fqn,
memberName: prop.name,
}, {
returnsUnion: prop.type,
});
this.emitStabilityAnnotations(prop);
if (prop.optional) {
if (prop.overrides) {
this.code.line('@Override');
}
this.code.openBlock(`default ${displayStatic(getterType)} get${propName}()`);
this.code.line('return null;');
this.code.closeBlock();
}
else {
this.code.line(`${displayStatic(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,
// Setter doesn't need a union type hint because we're generating overloads
});
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 ${displayStatic(type)} value)`);
this.code.line(`throw new UnsupportedOperationException("'void " + getClass().getCanonicalName() + "#set${propName}(${displayStatic(type)})' is not implemented!");`);
this.code.closeBlock();
}
else {
this.code.line(`void set${propName}(final ${displayStatic(type)} value);`);
}
}
}
}
/**
* Emits a local default implementation for properties inherited from multiple
* distinct parent types. This removes the default method dispatch ambiguity
* that would otherwise exist.
*
* @param ifc the interface to be processed.
*
* @see https://github.com/aws/jsii/issues/2256
*/
emitMultiplyInheritedProperties(ifc) {
if (ifc.interfaces == null || ifc.interfaces.length <= 1) {
// Nothing to do if we don't have parent interfaces, or if we have exactly one
return;
}
const memberSources = {};
for (const parent of ifc.interfaces) {
const type = this.reflectAssembly.system.findInterface(parent);
for (const prop of type.allProperties) {
if (!(prop.name in memberSources)) {
memberSources[prop.name] = {};
}
memberSources[prop.name][prop.definingType.fqn] = prop.spec;
}
}
for (const defininingTypes of Object.values(memberSources)) {
// Ignore our own type
delete defininingTypes[ifc.fqn];
const keys = Object.keys(defininingTypes);
if (keys.length > 1) {
this.onInterfaceProperty(ifc, defininingTypes[keys[0]]);
}
}
}
emitAssemblyPackageInfo(mod) {
if (!mod.docs) {
return;
}
const { packageName } = this.toNativeName(mod);
const packageInfoFile = this.toJavaFilePath(mod, `${mod.name}.package-info`);
this.code.openFile(packageInfoFile);
this.code.line('/**');
if (mod.readme) {
for (const line of myMarkDownToJavaDoc(this.convertSamplesInMarkdown(mod.readme.markdown, {
api: 'moduleReadme',
moduleFqn: mod.name,
})).split('\n')) {
this.code.line(` * ${line.replace(/\*\//g, '*{@literal /}')}`);
}
}
if (mod.docs.deprecated) {
this.code.line(' *');
// Javac won't allow @deprecated on packages, while @Deprecated is aaaabsolutely fine. Duh.
this.code.line(` * Deprecated: ${mod.docs.deprecated}`);
}
this.code.line(' */');
this.emitStabilityAnnotations(mod);
this.code.line(`package ${packageName};`);
this.code.closeFile(packageInfoFile);
}
emitSubmodulePackageInfo(assembly, moduleFqn) {
const mod = assembly.submodules?.[moduleFqn];
if (!mod?.readme?.markdown) {
return;
}
const { packageName } = translateFqn(assembly, moduleFqn);
const packageInfoFile = this.toJavaFilePath(assembly, `${moduleFqn}.package-info`);
this.code.openFile(packageInfoFile);
this.code.line('/**');
if (mod.readme) {
for (const line of myMarkDownToJavaDoc(this.convertSamplesInMarkdown(mod.readme.markdown, {
api: 'moduleReadme',
moduleFqn,
})).split('\n')) {
this.code.line(` * ${line.replace(/\*\//g, '*{@literal /}')}`);
}
}
this.code.line(' */');
this.code.line(`package ${packageName};`);
this.code.closeFile(packageInfoFile);
}
emitMavenPom(assm, fingerprint) {
if (!assm.targets?.java) {
throw new Error(`Assembly ${assm.name} does not declare a java target`);
}
const comment = fingerprint
? {
'#comment': [
`Generated by jsii-pacmak@${version_1.VERSION_DESC} on ${new Date().toISOString()}`,
`@jsii-pacmak:meta@ ${JSON.stringify(this.metadata)}`,
],
}
: {};
this.code.openFile('pom.xml');
this.code.line(xmlbuilder
.create({
project: {
'@xmlns': 'http://maven.apache.org/POM/4.0.0',
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'@xsi:schemaLocation': 'http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd',
...comment,
modelVersion: '4.0.0',
name: '${project.groupId}:${project.artifactId}',
description: assm.description,
url: assm.homepage,
licenses: {
license: getLicense(),
},
developers: {
developer: mavenDevelopers(),
},
scm: {
connection: `scm:${assm.repository.type}:${assm.repository.url}`,
url: assm.repository.url,
},
groupId: assm.targets.java.maven.groupId,
artifactId: assm.targets.java.maven.artifactId,
version: makeVersion(assm.version, assm.targets.java.maven.versionSuffix),
packaging: 'jar',
properties: { 'project.build.sourceEncoding': 'UTF-8' },
dependencies: { dependency: mavenDependencies.call(this) },
build: {
plugins: {
plugin: [
{
groupId: 'org.apache.maven.plugins',
artifactId: 'maven-compiler-plugin',
version: '3.11.0',
configuration: {
source: '1.8',
target: '1.8',
fork: 'true',
maxmem: '4096m',
},
},
{
groupId: 'org.apache.maven.plugins',
artifactId: 'maven-jar-plugin',
version: '3.3.0',
configuration: {
archive: {
index: true,
manifest: {
addDefaultImplementationEntries: true,
addDefaultSpecificationEntries: true,
},
},
},
},
{
groupId: 'org.apache.maven.plugins',
artifactId: 'maven-source-plugin',
version: '3.3.0',
executions: {
execution: {
id: 'attach-sources',
goals: { goal: 'jar' },
},
},
},
{
groupId: 'org.apache.maven.plugins',
artifactId: 'maven-javadoc-plugin',
version: '3.5.0',
executions: {
execution: {
id: 'attach-javadocs',
goals: { goal: 'jar' },
},
},
configuration: {
failOnError: false,
show: 'protected',
sourceFileExcludes: {
// Excluding the $Module classes so they won't pollute the docsite. They otherwise
// are all collected at the top of the classlist, burrying useful information under
// a lot of dry scrolling.
exclude: ['**/$Module.java'],
},
// Adding these makes JavaDoc generation about a 3rd faster (which is far and away the most
// expensive part of the build)
additionalJOption: [
'-J-XX:+TieredCompilation',
'-J-XX:TieredStopAtLevel=1',
],
doclint: 'none',
quiet: 'true',
},
},
{
groupId: 'org.apache.maven.plugins',
artifactId: 'maven-enforcer-plugin',
version: '3.3.0',
executions: {
execution: {
id: 'enforce-maven',
goals: { goal: 'enforce' },
configuration: {
rules: {
requireMavenVersion: { version: '3.6' },
},
},
},
},
},
{
groupId: 'org.codehaus.mojo',
artifactId: 'versions-maven-plugin',
version: '2.16.0',
configuration: {
generateBackupPoms: false,
},
},
],
},
},
},
}, { encoding: 'UTF-8' })
.end({ pretty: true }));
this.code.closeFile('pom.xml');
/**
* Combines a version number with an optional suffix. The suffix, when present, must begin with
* '-' or '.', and will be concatenated as-is to the version number..
*
* @param version the semantic version number
* @param suffix the suffix, if any.
*/
function makeVersion(version, suffix) {
if (!suffix) {
return (0, version_utils_1.toReleaseVersion)(version, index_1.TargetName.JAVA);
}
if (!suffix.startsWith('-') && !suffix.startsWith('.')) {
throw new Error(`versionSuffix must start with '-' or '.', but received ${suffix}`);
}
return `${version}${suffix}`;
}
function mavenDependencies() {
const dependencies = new Array();
for (const [depName, version] of Object.entries(this.assembly.dependencies ?? {})) {
const dep = this.assembly.dependencyClosure?.[depName];
if (!dep?.targets?.java) {
throw new Error(`Assembly ${assm.name} depends on ${depName}, which does not declare a java target`);
}
dependencies.push({
groupId: dep.targets.java.maven.groupId,
artifactId: dep.targets.java.maven.artifactId,
version: (0, version_utils_1.toMavenVersionRange)(version, dep.targets.java.maven.versionSuffix),
});
}
// The JSII java runtime base classes
dependencies.push({
groupId: 'software.amazon.jsii',
artifactId: 'jsii-runtime',
version: version_1.VERSION === '0.0.0'
? '[0.0.0-SNAPSHOT]'
: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
(0, version_utils_1.toMavenVersionRange)(`^${version_1.VERSION}`),
});
// Provides @org.jetbrains.* (compile-only: CLASS retention, no runtime behavior)
dependencies.push({
groupId: 'org.jetbrains',
artifactId: 'annotations',
version: '[16.0.3,20.0.0)',
scope: 'provided',
});
// Provides @javax.annotation.Generated for JDKs >= 9 (compile-only: SOURCE retention, not in bytecode)
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: 'provided',
});
return dependencies;
}
function mavenDevelopers() {
return [assm.author, ...(assm.contributors ?? [])].map(toDeveloper);
function toDeveloper(person) {
const developer = {
[person.organization ? 'organization' : 'name']: person.name,
roles: { role: person.roles },
};
// We cannot set "undefined" or "null" to a field - this causes invalid XML to be emitted (per POM schema).
if (person.email) {
developer.email = person.email;
}
if (person.url) {
developer[person.organization ? 'organizationUrl' : 'url'] =
person.url;
}
return developer;
}
}
/**
* Get the maven-style license block for a the assembly.
* @see https://maven.apache.org/pom.html#Licenses
*/
function getLicense() {
const spdx = spdxLicenseList[assm.license];
return (spdx && {
name: spdx.name,
url: spdx.url,
distribution: 'repo',
comments: spdx.osiApproved ? 'An OSI-approved license' : undefined,
});
}
}
emitStaticInitializer(cls) {
const consts = (cls.properties ?? []).filter((x) => x.const);
if (consts.length === 0) {
return;
}
const javaClass = forceSingleType(this.toJavaTypes(cls));
this.code.line();
this.code.openBlock('static');
for (const prop of consts) {
const constName = this.renderConstName(prop);
const propType = this.toNativeType(forceSingleType(this.toJavaTypes(prop.type)));
const statement = `software.amazon.jsii.JsiiObject.jsiiStaticGet(${displayStatic(javaClass)}.class, "${prop.name}", ${propType})`;
this.code.line(`${constName} = ${this.wrapCollection(statement, prop.type, prop.optional)};`);
}
this.code.closeBlock();
}
renderConstName(prop) {
return this.code.toSnakeCase(prop.name).toLocaleUpperCase(); // java consts are SNAKE_UPPER_CASE
}
emitConstProperty(parentType, prop) {
const propType = forceSingleType(this.toJavaTypes(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,
}, {
returnsUnion: prop.type,
});
this.emitStabilityAnnotations(prop);
this.code.line(`${access} final static ${displayStatic(propType)} ${propName};`);
}
emitProperty(cls, prop, definingType, { defaultImpl = false, final = false, includeGetter = true, overrides = !!prop.overrides, } = {}) {
const setterTypes = this.toDecoratedJavaTypes(prop);
const getterType = forceSingleType(setterTypes);
const propName = (0, naming_util_1.jsiiToPascalCase)(JavaGenerator.safeJavaPropertyName(prop.name));
const modifiers = [defaultImpl ? 'default' : this.renderAccessLevel(prop)];
if (prop.static)
modifiers.push('static');
if (prop.abstract && !defaultImpl)
modifiers.push('abstract');
if (final && !prop.abstract && !defaultImpl)
modifiers.push('final');
const javaClass = forceSingleType(this.toJavaTypes(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,
}, {
returnsUnion: prop.type,
});
if (overrides && !prop.static) {
this.code.line('@Override');
}
this.emitStabilityAnnotations(prop);
const signature = `${modifiers.join(' ')} ${displayStatic(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(${displayStatic(forceSingleType(this.toJavaTypes(cls)))}.class, `;
}
else {
statement = 'software.amazon.jsii.Kernel.get(this, ';
}
statement += `"${prop.name}", ${this.toNativeType(forceSingleType(this.toJavaTypes(prop.type)))})`;
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,
// No union type hint for setters
});
if (overrides && !prop.static) {
this.code.line('@Override');
}
this.emitStabilityAnnotations(prop);
// We could have emitted a covariant argument type here, but we didn't historically and I'm not changing that now.
const signature = `${modifiers.join(' ')} void set${propName}(final ${displayStatic(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 we emit code for `String instanceof Number`,
// which will always return `false`, the compiler would reject the code).
if (displayStatic(type).includes('java.lang.Object') &&
(!spec.isPrimitiveTypeReference(prop.type) ||
prop.type.primitive === spec.PrimitiveType.Any)) {
this.emitUnionParameterValidation([
{
name: 'value',
type: this.filterType(prop, type),
},
], 'exact-types');
}
if (prop.static) {
statement += `software.amazon.jsii.JsiiObject.jsiiStaticSet(${displayStatic(javaClass)}.class, `;
}
else {
statement += 'software.amazon.jsii.Kernel.set(this, ';
}
const value = prop.optional
? 'value'
: `java.util.Objects.requireNonNull(value, "${prop.name} is required")`;
statement += `"${prop.name}", ${value});`;
this.code.line(statement);
this.code.closeBlock();
}
}
}
}
/**
* Filters types from a union to select only those that correspond to the
* specified javaType.
*
* @param ref the type to be filtered.
* @param javaType the java type that is expected.
* @param covariant whether collections should use the covariant form.
* @param optional whether the type at an optional location or not
*
* @returns a type reference that matches the provided javaType.
*/
filterType(prop, javaType) {
if (!spec.isUnionTypeReference(prop.type)) {
// No filterning needed -- this isn't a type union!
return prop.type;
}
const types = prop.type.union.types.filter((t) => typesEqual(forceSingleType(annotateOptional(prop, this.toJavaTypes(t))), javaType));
(0, assert_1.default)(types.length > 0, `No type found in ${spec.describeTypeReference(prop.type)} has Java type ${displayStatic(javaType)}`);
return { union: { types } };
}
emitMethod(cls, method, { defaultImpl = false, final = false, overrides = !!method.overrides, } = {}) {
const returnType = method.returns
? forceSingleType(this.toDecoratedJavaTypes(method.returns))
: mkStatic('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 types = this.convertTypes(method.parameters);
const covariance = covarianceFromOverridability(overridabilityFromMethod(method));
const signature = `${typeVarDeclarations(types)}${displayStatic(returnType)} ${methodName}(${this.renderParameters(method.parameters, types, covariance)})`;
this.code.line();
this.addJavaDocs(method, {
api: 'member',
fqn: cls.fqn,
memberName: method.name,
}, {
returnsUnion: method.returns?.type,
});
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.emitUnionParameterValidation(method.parameters, covariance);
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.
*/
emitUnionParameterValidation(parameters, covariance) {
if (!this.runtimeTypeChecking) {
// We were configured not to emit those, so bail out now.
return;
}
const unionParameters = parameters?.filter(({ type }) => (0, type_utils_1.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.toSingleJavaType(param.type);
const asListName = `__${param.name}__asList`;
this.code.line(`final java.util.List<${displayStatic(javaType, covariance)}> ${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, false);
}
}
this.code.closeBlock();
function validate(value, descr, type, parameterName, 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(value, descr, elementType, parameterName, isRawArray) {
const suffix = (0, crypto_1.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.toSingleJavaType(elementType);
this.code.line(`final ${displayStatic(eltType)} ${valName} = ${value}.get(${idxName});`);
validate.call(this, valName, isRawArray
? `${descr}.append("[").append(${idxName}).append("]")`
: `${descr}.append(".get(").append(${idxName}).append(")")`, elementType, parameterName, false);
this.code.closeBlock();
}
function validateMap(value, descr, elementType, parameterName) {
// 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 = (0, crypto_1.createHash)('sha256')
.update(descr)
.digest('hex')
.slice(0, 6);
const varName = `__item_${suffix}`;
const valName = `__val_${suffix}`;
const javaElemType = this.toSingleJavaType(elementType);
const entryType = mkStatic('java.util.Map.Entry', [
mkStatic('java.lang.String'),
javaElemType,
]);
this.code.openBlock(`for (final ${displayStatic(entryType, covariance)} ${varName}: ${value}.entrySet())`);
this.code.line(`final ${displayStatic(javaElemType, covariance)} ${valName} = ${varName}.getValue();`);
validate.call(this, valName, `${descr}.append(".get(\\"").append((${varName}.getKey())).append("\\")")`, elementType, parameterName);
this.code.closeBlock();
}
function validateTypeUnion(value, descr, type, parameterName) {
let emitAnd = false;
const nestedCollectionUnionTypes = new Map();
const typeRefs = type.union.types;
if (typeRefs.length > 1) {
this.code.indent('if (');
}
const checked = new Set();
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.toSingleJavaType(typeRef);
if (javaRawType !== displayStatic(javaType)) {
nestedCollectionUnionTypes.set(displayStatic(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 `${displayStatic(this.toSingleJavaType(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_${(0, crypto_1.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.
*/
emitProxy(type) {
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(' */');
// Get the list of $Default interfaces
const baseInterfaces = this.defaultInterfacesFor(type, {
includeThisType: true,
});
// Add ourselves if we don't have a $Default interface
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(needsProxyImpl)) {
const prop = (0, clone_1.default)(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.flatMap(this.makeProxyImpls.bind(this))) {
const method = (0, clone_1.default)(reflectMethod);
// Emitting "final" since this is a proxy and nothing will/should override this
method.abstract = false;
this.emitMethod(type.spec, method, { final: true, overrides: true });
}
this.code.closeBlock();
}
emitDefaultImplementation(type) {
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(needsDefaultImpl)) {
this.emitProperty(type.spec, property.spec, property.definingType.spec, {
defaultImpl: true,
overrides: type.isInterfaceType(),
});
}
for (const method of type.allMethods.flatMap(this.makeDefaultImpls.bind(this))) {
this.emitMethod(type.spec, method, {
defaultImpl: true,
overrides: true,
});
}
this.code.closeBlock();
}
emitStabilityAnnotations(entity) {
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) {
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}`);
}
}
}
toJavaProp(property, definingType, inherited) {
const safeName = JavaGenerator.safeJavaPropertyName(property.name);
const propName = (0, naming_util_1.jsiiToPascalCase)(safeName);
const noIntersectionJavaTypes = this.toJavaTypes(removeIntersections(property.type));
const singleJavaType = forceSingleType(noIntersectionJavaTypes);
return {
docs: property.docs,
spec: property,
definingType,
propName,
jsiiName: property.name,
nullable: !!property.optional,
fieldName: this.code.toCamelCase(safeName),
renderedFieldType: displayStatic(singleJavaType),
renderedParamType: displayType(singleJavaType, 'covariant'),
renderedNativeFieldType: this.toNativeType(singleJavaType),
javaTypes: this.toJavaTypes(property.type),
immutable: property.immutable ?? false,
inherited,
};
}
emitClassBuilder(cls) {
// 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.fqn);
const structParamName = this.code.toCamelCase(JavaGenerator.safeJavaPropertyName(firstStruct.name));
const structBuilder = `${displayStatic(this.toSingleJavaType(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.toSingleJavaType(param.type),
}));
const builtType = this.toSingleJavaType(cls);
this.code.line();
this.code.line('/**');
// eslint-disable-next-line prettier/prettier
this.code.line(` * ${(0, _utils_1.stabilityPrefixFor)(cls.initializer)}A fluent builder for {@link ${displayStatic(builtType)}}.`);
this.code.line(' */');
this.emitStabilityAnnotations(cls.initializer);
this.code.openBlock(`public static final class ${BUILDER_CLASS_NAME} implements software.amazon.jsii.Builder<${displayStatic(builtType)}>`);
// Static factory method(s)
for (const params of computeOverrides(positionalParams)) {
const dummyMethod = {
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 ${displayStatic(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 ${displayStatic(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 ${displayStatic(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 = {
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)) {
this.addJavaDocs(setter, {
api: 'member',
fqn: prop.definingType.fqn, // Could be inherited
memberName: prop.name,
});
this.emitStabilityAnnotations(prop.spec);
// FIXME: generix
const renderedType = displayType(javaType, 'covariant');
this.code.openBlock(`public ${typeVarDeclarations([javaType])}${BUILDER_CLASS_NAME} ${fieldName}(final ${renderedType} ${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 ${displayStatic(builtType)}}.`);
this.code.line(' */');
this.emitStabilityAnnotations(cls.initializer);
this.code.line('@Override');
this.code.openBlock(`public ${displayStatic(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 ${displayStatic(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();
}
emitBuilderSetter(prop, builderName, parentType) {
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(` * ${this.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);
const renderedType = displayType(type, 'covariant');
// We add an explicit cast if both types are generic but they are not identical (one is covariant, the other isn't)
const explicitCast = renderedType.includes('<') &&
prop.renderedFieldType.includes('<') &&
renderedType !== prop.renderedFieldType
? `(${prop.renderedFieldType})`
: '';
if (explicitCast !== '') {
// We'll be doing a safe, but unchecked cast, so suppress that warning
this.code.line('@SuppressWarnings("unchecked")');
}
this.code.openBlock(`public ${typeVarDeclarations([type])}${builderName} ${prop.fieldName}(${renderedType} ${prop.fieldName})`);
this.code.line(`this.${prop.fieldName} = ${explicitCast}${prop.fieldName};`);
this.code.line('return this;');
this.code.closeBlock();
}
function getterFor(fieldName) {
const [first, ...rest] = fieldName;
return `get${first.toUpperCase()}${rest.join('')}`;
}
}
emitInterfaceBuilder(classSpec, constructorName, props) {
// 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.renderedFieldType} ${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
}
emitDataType(ifc) {
// 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 = {};
function collectProps(currentIfc, 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), 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.renderedFieldType} ${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.renderedNativeFieldType});`));
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.renderedFieldType !== prop.renderedParamType)) {
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.renderedFieldType !== prop.renderedParamType
? `(${prop.renderedFieldType})`
: '';
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.renderedFieldType} 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, prop) {
if (prop.nullable) {
return variable;
}
return `java.util.Objects.requireNonNull(${variable}, "${prop.fieldName} is required")`;
}
}
emitEqualsOverride(className, props) {
// 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 java.lang.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();
}
emitHashCodeOverride(props) {
// 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) {
return prop.nullable
? `this.${prop.fieldName} != null ? this.${prop.fieldName}.hashCode() : 0`
: `this.${prop.fieldName}.hashCode()`;
}
}
openFileIfNeeded(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();
}
closeFileIfNeeded(type) {
if (this.isNested(type)) {
return;
}
this.code.closeFile(this.toJavaFilePath(this.assembly, type));
}
isNested(type) {
if (!this.assembly.types || !type.namespace) {
return false;
}
const parent = `${type.assembly}.${type.namespace}`;
return parent in this.assembly.types;
}
toJavaFilePath(assm, what) {
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`;
}
toJavaResourcePath(assm, fqn, 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
addJavaDocs(doc, apiLoc, unionHint) {
const returnsUnion = unionHint?.returnsUnion && (0, type_utils_1.containsUnionType)(unionHint.returnsUnion)
? this.renderTypeReference(unionHint.returnsUnion)
: undefined;
if (!returnsUnion &&
Object.keys(doc.docs ?? {}).length === 0 &&
!(doc.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((0, _utils_1.renderSummary)(docs))));
}
if (docs.remarks) {
paras.push(myMarkDownToJavaDoc(this.convertSamplesInMarkdown(docs.remarks, apiLoc)).trimRight());
}
if (returnsUnion) {
paras.push(`Returns union: ${returnsUnion}`);
}
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.parameters) {
const method = doc;
if (method.parameters) {
for (const param of method.parameters) {
const summary = param.docs?.summary;
tagLines.push(this.paramJavadoc(param.name, param.optional, summary, param.type));
}
}
}
if (tagLines.length > 0) {
paras.push(tagLines.join('\n'));
}
const lines = new Array();
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(' */');
}
paramJavadoc(name, optional, summary, unionTypeHint) {
const parts = ['@param', name];
if (summary) {
parts.push(stripNewLines(myMarkDownToJavaDoc(endWithPeriod(summary))));
}
if (unionTypeHint && (0, type_utils_1.containsUnionType)(unionTypeHint)) {
parts.push(`Takes union: ${this.renderTypeReference(unionTypeHint)}.`);
}
if (!optional) {
parts.push('This parameter is required.');
}
return parts.join(' ');
}
getClassBase(cls) {
if (!cls.base) {
return mkStatic('software.amazon.jsii.JsiiObject');
}
return this.toJavaTypes({ fqn: cls.base })[0];
}
/**
* Convert an OptionalValue, adding the `NotNull` or `Nullable` declarations as appropriate
*/
toDecoratedJavaTypes(optionalValue) {
return annotateOptional(optionalValue, this.toJavaTypes(optionalValue.type));
}
/**
* Renders a type without generic arguments
*
* 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`.
*/
toJavaTypeNoGenerics(type) {
return forceSingleType(this.toJavaTypes(type)).typeSymbol;
}
/**
* Return the jsii object that represents this native type
*/
toNativeType(type) {
if (type.type === 'static' && type.typeSymbol === 'java.util.List') {
const nativeElementType = this.toNativeType(type.typeArguments[0]);
return `software.amazon.jsii.NativeType.listOf(${nativeElementType})`;
}
if (type.type === 'static' && type.typeSymbol === 'java.util.Map') {
const nativeElementType = this.toNativeType(type.typeArguments[1]);
return `software.amazon.jsii.NativeType.mapOf(${nativeElementType})`;
}
return `software.amazon.jsii.NativeType.forClass(${marshallingType(type)})`;
}
/**
* Convert a jsii type to a single Java type, collapsing to object if necessary
*/
toSingleJavaType(typeref, { typeSymGen = undefined } = {}) {
return forceSingleType(this.toJavaTypes(typeref, { typeSymGen }));
}
/**
* Convert a jsii type to a union of Java types
*/
toJavaTypes(typeref, { typeSymGen = undefined } = {}) {
typeSymGen = typeSymGen ?? newTypeSymGen();
if (spec.isPrimitiveTypeReference(typeref)) {
return [this.toJavaPrimitive(typeref.primitive)];
}
else if (spec.isCollectionTypeReference(typeref)) {
return [
this.toJavaCollection(typeref, {
typeSymGen,
}),
];
}
else if (spec.isNamedTypeReference(typeref)) {
const literal = (0, type_literals_1.literalTypeReference)(typeref);
return literal
? [mkStatic(literal)]
: [mkStatic(this.toNativeFqn(typeref.fqn))];
}
else if (spec.isUnionTypeReference(typeref)) {
return typeref.union.types.flatMap((subtype) => this.toJavaTypes(subtype, { typeSymGen }));
}
else if (spec.isIntersectionTypeReference(typeref)) {
const typeVariable = typeSymGen();
return [
mkParam(typeVariable, `${typeVariable} extends ${typeref.intersection.types
// Take the first type of any given union (an intersect-of-union
// would have been rejected by jsii-compiler anyway)
.map((t) => displayStatic(this.toJavaTypes(t)[0]))
.join(' & ')}`),
];
}
throw new Error(`Unrenderable type reference: ${JSON.stringify(typeref)}`);
}
toJavaCollection(ref, { typeSymGen }) {
const elementJavaType = this.toSingleJavaType(ref.collection.elementtype, {
typeSymGen,
});
switch (ref.collection.kind) {
case spec.CollectionKind.Array:
return mkStatic('java.util.List', [elementJavaType]);
case spec.CollectionKind.Map:
return mkStatic('java.util.Map', [
mkStatic('java.lang.String'),
elementJavaType,
]);
default:
throw new Error(`Unsupported collection kind: ${ref.collection.kind}`);
}
}
toJavaPrimitive(primitive) {
switch (primitive) {
case spec.PrimitiveType.Boolean:
return mkStatic('java.lang.Boolean');
case spec.PrimitiveType.Date:
return mkStatic('java.time.Instant');
case spec.PrimitiveType.Json:
return mkStatic('com.fasterxml.jackson.databind.node.ObjectNode');
case spec.PrimitiveType.Number:
return mkStatic('java.lang.Number');
case spec.PrimitiveType.String:
return mkStatic('java.lang.String');
case spec.PrimitiveType.Any:
return mkStatic('java.lang.Object');
default:
throw new Error(`Unknown primitive type: ${primitive}`);
}
}
/**
* Render a type reference to something human readable for use in JavaDocs
*/
renderTypeReference(x) {
return (0, type_visitor_1.visitTypeReference)(x, {
primitive: (x) => `{@link ${this.toJavaPrimitive(x.primitive).typeSymbol}}`,
named: (x) => `{@link ${this.toNativeFqn(x.fqn)}}`,
collection: (x) => {
switch (x.collection.kind) {
case spec.CollectionKind.Array:
return `List<${this.renderTypeReference(x.collection.elementtype)}>`;
case spec.CollectionKind.Map:
return `Map<String, ${this.renderTypeReference(x.collection.elementtype)}>`;
}
},
union: (x) => `either ${x.union.types.map((x) => this.renderTypeReference(x)).join(' or ')}`,
intersection: (x) => `${x.intersection.types.map((x) => this.renderTypeReference(x)).join(' + ')}`,
});
}
renderMethodCallArguments(method) {
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) {
const safeName = JavaGenerator.safeJavaPropertyName(param.name);
return isNullable(param)
? safeName
: `java.util.Objects.requireNonNull(${safeName}, "${safeName} is required")`;
}
}
renderMethodCall(cls, method, async) {
let statement = '';
if (method.static) {
const javaClass = this.toSingleJavaType(cls);
statement += `software.amazon.jsii.JsiiObject.jsiiStaticCall(${displayStatic(javaClass)}.class, `;
}
else {
statement += `software.amazon.jsii.Kernel.${async ? 'asyncCall' : 'call'}(this, `;
}
statement += `"${method.name}"`;
if (method.returns) {
const returnsType = forceSingleType(this.toJavaTypes(method.returns.type));
statement += `, ${this.toNativeType(returnsType)}`;
}
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.
*/
wrapCollection(statement, type, optional) {
if (spec.isCollectionTypeReference(type)) {
let wrapper;
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}`);
}
// 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((${displayStatic(this.toSingleJavaType(type))})(${statement})).map(java.util.Collections::${wrapper}).orElse(null)`
: `java.util.Collections.${wrapper}(${statement})`;
}
return statement;
}
convertTypes(parameters) {
return (parameters ?? []).map((p) => {
return forceSingleType(this.toDecoratedJavaTypes(p));
});
}
renderParameters(parameters, types, cov) {
parameters = parameters ?? [];
if (parameters.length !== types.length) {
throw new Error(`Arrays not same length: ${parameters.length} !== ${types.length}`);
}
const params = [];
for (const [p, type] of (0, util_1.zip)(parameters, types)) {
params.push(`final ${displayType(type, cov)}${p.variadic ? '...' : ''} ${JavaGenerator.safeJavaPropertyName(p.name)}`);
}
return params.join(', ');
}
renderAccessLevel(method) {
return method.protected ? 'protected' : 'public';
}
makeModuleClass(moduleName) {
return `${this.toNativeFqn(moduleName)}.${MODULE_CLASS_NAME}`;
}
emitModuleFile(mod) {
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;
}
/**
* Given a method, return the methods that we should generate implementations for on the $Default interface
*
* This can be 0..N:
*
* - 0: if the method can be inherited from a parent $Default implementation
* - 1: if the method cannot be inherited from a parent $Default implementation
* - N: ah-ha, wait! There can be overloads! And because of a historical bug,
* we didn't use to generate overloads onto $Default interfaces. So it's possible
* that we don't generate the "main" implementation, but we do generate its overloads.
*
* Technically speaking we only have to account for the bug if the type is from a different
* assembly (because we know all types from the current assembly will be generated ✨ bugless ✨,
* but just to keep it simple we'll always do the same thing).
*
* We can only get rid of this bug once the oldest dependency package a Java
* package can be used with definitely has overloaded $Default impls. So that will be a while.
*/
makeDefaultImpls(m) {
const ret = [];
if (needsDefaultImpl(m)) {
ret.push(m.spec);
}
// Account for a past bug
if (needsDefaultImpl(m) || GENERATE_POTENTIALLY_MISING_DEFAULT_OVERLOADS) {
ret.push(...this.createOverloadsForOptionals(m.spec));
}
return ret;
}
/**
* Given a method, return the methods that we should generate implementations for on the $Proxy class
*
* See `makeDefaultImpls` for the main rationale behind this. The $Proxy class inherits from $Default
* so technically this could have usually been empty, but we need to account for the possibility that
* we implement a $Default interface from another assembly that has been generated with a buggy version
* of pacmak.
*/
makeProxyImpls(m) {
const ret = [];
if (needsProxyImpl(m)) {
ret.push(m.spec);
}
// Account for a past bug
if (needsProxyImpl(m) || GENERATE_POTENTIALLY_MISING_DEFAULT_OVERLOADS) {
ret.push(...this.createOverloadsForOptionals(m.spec));
}
return ret;
}
emitJsiiInitializers(cls) {
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.
*/
toNativeFqn(fqn, { binaryName } = { binaryName: false }) {
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}` : ''}`;
}
toNativeName(assm, type) {
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.
*/
emitGeneratedAnnotation() {
const date = this.emitFullGeneratorInfo
? `, date = "${new Date().toISOString()}"`
: '';
const generator = this.emitFullGeneratorInfo
? `jsii-pacmak/${version_1.VERSION_DESC}`
: 'jsii-pacmak';
this.code.line(`@javax.annotation.Generated(value = "${generator}"${date})`);
}
convertExample(example, api) {
(0, rosetta_assembly_1.assertSpecIsRosettaCompatible)(this.assembly);
const translated = this.rosetta.translateExample(api, example, jsii_rosetta_1.TargetLanguage.JAVA, (0, jsii_rosetta_1.enforcesStrictMode)(this.assembly));
return translated.source;
}
convertSamplesInMarkdown(markdown, api) {
(0, rosetta_assembly_1.assertSpecIsRosettaCompatible)(this.assembly);
return this.rosetta.translateSnippetsInMarkdown(api, markdown, jsii_rosetta_1.TargetLanguage.JAVA, (0, jsii_rosetta_1.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.
*/
defaultInterfacesFor(type, { includeThisType = false } = {}) {
const result = new Set();
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);
}
}
// 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.
JavaGenerator.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',
'_',
];
/**
* 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) {
if (!optionalValue) {
return false;
}
return (optionalValue.optional ||
(spec.isPrimitiveTypeReference(optionalValue.type) &&
optionalValue.type.primitive === spec.PrimitiveType.Any));
}
function endWithPeriod(s) {
s = s.trimRight();
if (!s.endsWith('.')) {
return `${s}.`;
}
return s;
}
function computeOverrides(allParams) {
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;
}
}
},
};
}
/**
* Return the native package name from an assembly
*/
function packageNameFromAssembly(assm) {
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, originalFqn) {
const implicitPackageNames = new Array();
const typeNames = new Array();
// 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(`.${(0, case_utils_1.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) {
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) {
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) {
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) {
return s.replace(/["\\<>&]/g, (c) => `&#${c.charCodeAt(0)};`);
}
function myMarkDownToJavaDoc(source) {
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 `{@`, which breaks the JavaDoc markup.
//
// Since docstring do not (or at least should not) contain JavaDoc literals, this is safe.
return source;
}
return (0, jsii_rosetta_1.markDownToJavaDoc)(source);
}
function stripNewLines(x) {
return x.replace(/\n/g, '');
}
// Replace */ with *\/ to avoid closing the comment block
function escapeEndingComment(x) {
return x.replace(/\*\//g, '*\\/');
}
function newTypeSymGen() {
let cnt = 1;
return () => {
return `T${cnt++}`;
};
}
function mkStatic(typeSymbol, typeArguments = []) {
return { typeSymbol, annotation: '', type: 'static', typeArguments };
}
function mkParam(typeVariable, typeVarDeclaration) {
return {
type: 'param',
annotation: '',
typeSymbol: typeVariable,
typeVariable,
typeVarDeclaration,
};
}
function displayStatic(x, options) {
if (x.type !== 'static') {
throw new Error(`Expected static type here, got ${JSON.stringify(x)}`);
}
return displayType(x, options);
}
function displayType(x, options) {
switch (x.type) {
case 'param':
return `${x.annotation}${x.typeSymbol}`;
case 'static':
return [
`${x.annotation}${x.typeSymbol}`,
(x.typeArguments ?? []).length > 0
? `<${x
.typeArguments.map((t, i) => {
const subT = displayType(t, options);
// For Map<K, V> we only want to make V covariant.
// Since that's and List<T> are the only types we have generics
// for anyway, we hack it here (as opposed to storing a field on
// the type object itself). This is simpler and Good Enough For Now(tm).
const isLastParameter = i === (x.typeArguments ?? []).length - 1;
// Don't emit a covariant expression for String (it's `final` in Java)
const isString = subT === 'java.lang.String';
return options === 'covariant' && isLastParameter && !isString
? `? extends ${subT}`
: subT;
})
.join(', ')}>`
: '',
].join('');
}
}
function typeVarDeclarations(types) {
const ret = [];
for (const t of types) {
recurse(t);
}
return ret.length > 0 ? `<${ret.join(', ')}> ` : '';
function recurse(t) {
switch (t.type) {
case 'param':
ret.push(t.typeVarDeclaration);
break;
case 'static':
for (const arg of t.typeArguments ?? []) {
recurse(arg);
}
break;
}
}
}
function typesEqual(a, b, opts) {
if (a.type !== b.type) {
return false;
}
if (a.typeSymbol !== b.typeSymbol) {
return false;
}
if (a.type === 'static' && b.type === 'static' && opts === 'deep') {
if (a.typeArguments?.length !== b.typeArguments?.length) {
return false;
}
if (a.typeArguments) {
for (let i = 0; i < a.typeArguments.length; i++) {
if (!typesEqual(a.typeArguments[i], b.typeArguments[i])) {
return false;
}
}
}
}
return true;
}
// endregion
/**
* Return a reference for the marshalling class object
*
* Basically returns `Type.class`, disregarding any generic
* type arguments.
*/
function marshallingType(x) {
switch (x.type) {
case 'static':
return `${x.typeSymbol}.class`;
case 'param':
throw new Error(`Cannot marshall a type parameter: ${JSON.stringify(x)}`);
}
}
/**
* Return the one type from a union, or collapse to Object if there's more than one type in the union
*/
function forceSingleType(types) {
if (types.length > 1) {
return {
...mkStatic('java.lang.Object'),
// Copy the annotation
annotation: types[0].annotation,
};
}
return types[0];
}
function annotateOptional(optionalValue, types) {
return types.map((t) => ({
...t,
annotation: `${optionalValue.optional ? ANN_NULLABLE : ANN_NOT_NULL} `,
}));
}
/**
* Resolve any type intersections to the first type
*/
function removeIntersections(x) {
if (spec.isIntersectionTypeReference(x)) {
return x.intersection.types[0];
}
if (spec.isUnionTypeReference(x)) {
return {
...x,
union: {
types: x.union.types.map(removeIntersections),
},
};
}
if (spec.isCollectionTypeReference(x)) {
return {
...x,
collection: {
kind: x.collection.kind,
elementtype: removeIntersections(x.collection.elementtype),
},
};
}
return x;
}
/**
* Whether the given property or method needs to be implemented on a $Proxy class
*
* Proxies extend the class they're for (if for a class), and the $Default interfaces
* of all the base interfaces, so implementations need to be present for everything
* that is not abstract and can be inherited from a $Default interface.
*
* Except we re-add potential overrides of Java Object members. We *can*
* implement these on classes, just not on interfaces.
*/
function needsProxyImpl(x) {
// Interface members are always marked 'abstract', but we only need to
// implement them if they come from a class (because interface members
// will have a $Default impl that calls out to jsii already).
const isAbstractClassMember = x.definingType.isClassType() && x.abstract;
return (isAbstractClassMember ||
!hasDefaultInterfaces(x.definingType.assembly) ||
isJavaLangObjectBuiltin(x));
}
/**
* Whether the given property or method needs to be implemented on a $Default interface
*
* $Default interfaces extend the interface they're for, and the $Default interfaces
* of all the base interfaces, so implementations need to be present for everything
* that is defined on the current interface or cannot be inherited from a $Default interface.
*
* We exclude built-in Java 'Object' methods. It's not allowed to write default
* implementations for these (think 'toString', 'hashCode', etc.)
*/
function needsDefaultImpl(x) {
return ((!hasDefaultInterfaces(x.definingType.assembly) ||
x.definingType.fqn === x.parentType.fqn) &&
!isJavaLangObjectBuiltin(x));
}
/**
* Whether the rendering of the given property or method already appears on java.lang.Object
*/
function isJavaLangObjectBuiltin(x) {
return x instanceof reflect.Property
? // Only checking the getter - java.lang.Object has no setters.
isJavaLangObjectMethodName(`get${(0, naming_util_1.jsiiToPascalCase)(x.name)}`)
: isJavaLangObjectMethodName(x.name);
}
/**
* Return the appropriate covariance rendering for the overridability of a given method
*/
function covarianceFromOverridability(overridable) {
// We can render covariant parameters only for methods that aren't overridable... so only for static methods currently.
// (There are some more places where we could do this, like properties and constructors, but historically we didn't
// and I don't want to mess with this too much because the risk/reward isn't there.)
return overridable === 'final' ? 'covariant' : 'exact-types';
}
function overridabilityFromMethod(method) {
return method.static ? 'final' : 'overridable';
}
//# sourceMappingURL=java.js.map