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