import type { XmlDocument } from 'xmldoc'; import { logger } from '../../../logger/index.ts'; import * as packageCache from '../../../util/cache/package/index.ts'; import { asTimestamp } from '../../../util/timestamp.ts'; import { ensureTrailingSlash } from '../../../util/url.ts'; import { compare } from '../../versioning/maven/compare.ts'; import mavenVersion, * as mavenVersioning from '../../versioning/maven/index.ts'; import { Datasource } from '../datasource.ts'; import type { GetReleasesConfig, PostprocessReleaseConfig, PostprocessReleaseResult, RegistryStrategy, Release, ReleaseResult, } from '../types.ts'; import { MAVEN_CENTRAL_URLS, MAVEN_REPO } from './common.ts'; import type { MavenDependency, MetadataResults } from './types.ts'; import { createUrlForDependencyPom, downloadMaven, downloadMavenXml, getDependencyInfo, getDependencyParts, getMavenUrl, } from './util.ts'; function getLatestSuitableVersion(releases: Release[]): string | null { /* v8 ignore next -- TODO: add test */ if (!releases?.length) { return null; } const allVersions = releases.map(({ version }) => version); const stableVersions = allVersions.filter((x) => mavenVersion.isStable(x)); const versions = stableVersions.length ? stableVersions : allVersions; return versions.reduce((latestVersion, version) => compare(version, latestVersion) === 1 ? version : /* istanbul ignore next: hard to test */ latestVersion, ); } function extractVersions(metadata: XmlDocument): MetadataResults { const res: MetadataResults = {}; const versions = metadata.descendantWithPath('versioning.versions'); const elements = versions?.childrenNamed('version'); if (!elements) { return res; } res.versions = elements.map((el) => el.val); const latest = metadata.descendantWithPath('versioning.latest'); if (latest?.val) { res.tags ??= {}; res.tags.latest = latest.val; } const release = metadata.descendantWithPath('versioning.release'); if (release?.val) { res.tags ??= {}; res.tags.release = release.val; } return res; } export const defaultRegistryUrls = [MAVEN_REPO]; export class MavenDatasource extends Datasource { static id = 'maven'; override readonly caching = true; override readonly defaultRegistryUrls = defaultRegistryUrls; override readonly defaultVersioning: string = mavenVersioning.id; override readonly registryStrategy: RegistryStrategy = 'merge'; override readonly releaseTimestampSupport = true; override readonly releaseTimestampNote = 'The release timestamp is determined from the `Last-Modified` header or the `lastModified` field in the results.'; override readonly sourceUrlSupport = 'package'; override readonly sourceUrlNote = 'The source URL is determined from the `scm` tags in the results.'; constructor(id = MavenDatasource.id) { super(id); } async fetchVersionsFromMetadata( dependency: MavenDependency, repoUrl: string, ): Promise { const metadataUrl = getMavenUrl(dependency, repoUrl, 'maven-metadata.xml'); const metadataXmlResult = await downloadMavenXml(this.http, metadataUrl); return metadataXmlResult .transform(({ data: metadata }) => extractVersions(metadata)) .onError((err) => { logger.debug( `Maven: error fetching versions for "${dependency.display}": ${err.type}`, ); }) .unwrapOr({}); } async getReleases({ packageName, registryUrl, }: GetReleasesConfig): Promise { /* v8 ignore next -- should never happen */ if (!registryUrl) { return null; } const dependency = getDependencyParts(packageName); const repoUrl = ensureTrailingSlash(registryUrl); if ( // groupId (packageName.includes('.gradle.plugin:') || // artifactId packageName.endsWith('.gradle.plugin')) && MAVEN_CENTRAL_URLS.some((url) => repoUrl === ensureTrailingSlash(url)) ) { logger.debug( `Maven: skipping Maven Central for suspected Gradle plugin "${packageName}"`, ); return null; } logger.debug(`Looking up ${dependency.display} in repository ${repoUrl}`); const metadata = await this.fetchVersionsFromMetadata(dependency, repoUrl); if (!metadata.versions?.length) { return null; } const releases = metadata.versions.map((version) => ({ version })); logger.debug( `Found ${releases.length} new releases for ${dependency.display} in repository ${repoUrl}`, ); const latestSuitableVersion = getLatestSuitableVersion(releases); const dependencyInfo = latestSuitableVersion && (await getDependencyInfo( this.http, dependency, repoUrl, latestSuitableVersion, )); const result: ReleaseResult = { ...dependency, ...dependencyInfo, releases, }; if (metadata.tags) { result.tags = metadata.tags; if (result.tags.latest) { logger.debug(`Setting respectLatest=false for maven ${packageName}`); result.respectLatest = false; } } if (!this.defaultRegistryUrls.includes(registryUrl)) { result.isPrivate = true; } return result; } override async postprocessRelease( { packageName, registryUrl }: PostprocessReleaseConfig, release: Release, ): Promise { const { version, versionOrig } = release; const cacheKey = versionOrig ? `postprocessRelease:${registryUrl}:${packageName}:${versionOrig}:${version}` : `postprocessRelease:${registryUrl}:${packageName}:${version}`; const cachedResult = await packageCache.get( 'datasource-maven:postprocess-reject', cacheKey, ); /* v8 ignore if: hard to test */ if (cachedResult) { return cachedResult; } if (!packageName || !registryUrl) { return release; } const dependency = getDependencyParts(packageName); const pomUrl = await createUrlForDependencyPom( this.http, release.versionOrig ?? release.version, dependency, registryUrl, ); const artifactUrl = getMavenUrl(dependency, registryUrl, pomUrl); const fetchResult = await downloadMaven(this.http, artifactUrl); const { val, err } = fetchResult.unwrap(); if (err) { const result: PostprocessReleaseResult = err.type === 'not-found' ? 'reject' : release; if (result === 'reject') { await packageCache.set( 'datasource-maven:postprocess-reject', cacheKey, result, 24 * 60, ); } return result; } if (val.lastModified) { release.releaseTimestamp = asTimestamp(val.lastModified); } return release; } }