Files
smartregistry/ts/maven/helpers.maven.ts

347 lines
10 KiB
TypeScript
Raw Normal View History

/**
* Maven helper utilities
* Path conversion, XML generation, checksum calculation
*/
import * as plugins from '../plugins.js';
import type {
IMavenCoordinate,
IMavenMetadata,
IChecksums,
IMavenPom,
} from './interfaces.maven.js';
/**
* Convert Maven GAV coordinates to storage path
* Example: com.example:my-lib:1.0.0 com/example/my-lib/1.0.0
*/
export function gavToPath(
groupId: string,
artifactId: string,
version?: string
): string {
const groupPath = groupId.replace(/\./g, '/');
if (version) {
return `${groupPath}/${artifactId}/${version}`;
}
return `${groupPath}/${artifactId}`;
}
/**
* Parse Maven path to GAV coordinates
* Example: com/example/my-lib/1.0.0/my-lib-1.0.0.jar {groupId, artifactId, version, ...}
*/
export function pathToGAV(path: string): IMavenCoordinate | null {
// Remove leading slash if present
const cleanPath = path.startsWith('/') ? path.substring(1) : path;
// Split path into parts
const parts = cleanPath.split('/');
if (parts.length < 4) {
return null; // Not a valid artifact path
}
// Last part is filename
const filename = parts[parts.length - 1];
const version = parts[parts.length - 2];
const artifactId = parts[parts.length - 3];
const groupId = parts.slice(0, -3).join('.');
// Parse filename to extract classifier and extension
const parsed = parseFilename(filename, artifactId, version);
if (!parsed) {
return null;
}
return {
groupId,
artifactId,
version,
classifier: parsed.classifier,
extension: parsed.extension,
};
}
/**
* Parse Maven artifact filename
* Example: my-lib-1.0.0-sources.jar {classifier: 'sources', extension: 'jar'}
* Example: my-lib-1.0.0.jar.md5 {extension: 'md5'}
*/
export function parseFilename(
filename: string,
artifactId: string,
version: string
): { classifier?: string; extension: string } | null {
// Expected format: {artifactId}-{version}[-{classifier}].{extension}[.checksum]
const prefix = `${artifactId}-${version}`;
if (!filename.startsWith(prefix)) {
return null;
}
let remainder = filename.substring(prefix.length);
// Check if this is a checksum file (double extension like .jar.md5)
const checksumExtensions = ['md5', 'sha1', 'sha256', 'sha512'];
const lastDotIndex = remainder.lastIndexOf('.');
if (lastDotIndex !== -1) {
const possibleChecksum = remainder.substring(lastDotIndex + 1);
if (checksumExtensions.includes(possibleChecksum)) {
// This is a checksum file - just return the checksum extension
// The base artifact extension doesn't matter for checksum retrieval
return { extension: possibleChecksum };
}
}
// Regular artifact file parsing
const dotIndex = remainder.lastIndexOf('.');
if (dotIndex === -1) {
return null; // No extension
}
const extension = remainder.substring(dotIndex + 1);
const classifierPart = remainder.substring(0, dotIndex);
if (classifierPart.length === 0) {
// No classifier
return { extension };
}
if (classifierPart.startsWith('-')) {
// Has classifier
const classifier = classifierPart.substring(1);
return { classifier, extension };
}
return null; // Invalid format
}
/**
* Build Maven artifact filename
* Example: {artifactId: 'my-lib', version: '1.0.0', classifier: 'sources', extension: 'jar'}
* 'my-lib-1.0.0-sources.jar'
*/
export function buildFilename(coordinate: IMavenCoordinate): string {
const { artifactId, version, classifier, extension } = coordinate;
let filename = `${artifactId}-${version}`;
if (classifier) {
filename += `-${classifier}`;
}
filename += `.${extension}`;
return filename;
}
/**
* Calculate checksums for Maven artifact
* Returns MD5, SHA-1, SHA-256, SHA-512
*/
export async function calculateChecksums(data: Buffer): Promise<IChecksums> {
const crypto = await import('crypto');
return {
md5: crypto.createHash('md5').update(data).digest('hex'),
sha1: crypto.createHash('sha1').update(data).digest('hex'),
sha256: crypto.createHash('sha256').update(data).digest('hex'),
sha512: crypto.createHash('sha512').update(data).digest('hex'),
};
}
/**
* Generate maven-metadata.xml from metadata object
*/
export function generateMetadataXml(metadata: IMavenMetadata): string {
const { groupId, artifactId, versioning } = metadata;
const { latest, release, versions, lastUpdated, snapshot, snapshotVersions } = versioning;
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml += '<metadata>\n';
xml += ` <groupId>${escapeXml(groupId)}</groupId>\n`;
xml += ` <artifactId>${escapeXml(artifactId)}</artifactId>\n`;
// Add version if SNAPSHOT
if (snapshot) {
const snapshotVersion = versions[versions.length - 1]; // Assume last version is the SNAPSHOT
xml += ` <version>${escapeXml(snapshotVersion)}</version>\n`;
}
xml += ' <versioning>\n';
if (latest) {
xml += ` <latest>${escapeXml(latest)}</latest>\n`;
}
if (release) {
xml += ` <release>${escapeXml(release)}</release>\n`;
}
xml += ' <versions>\n';
for (const version of versions) {
xml += ` <version>${escapeXml(version)}</version>\n`;
}
xml += ' </versions>\n';
xml += ` <lastUpdated>${lastUpdated}</lastUpdated>\n`;
// Add SNAPSHOT info if present
if (snapshot) {
xml += ' <snapshot>\n';
xml += ` <timestamp>${escapeXml(snapshot.timestamp)}</timestamp>\n`;
xml += ` <buildNumber>${snapshot.buildNumber}</buildNumber>\n`;
xml += ' </snapshot>\n';
}
// Add SNAPSHOT versions if present
if (snapshotVersions && snapshotVersions.length > 0) {
xml += ' <snapshotVersions>\n';
for (const sv of snapshotVersions) {
xml += ' <snapshotVersion>\n';
if (sv.classifier) {
xml += ` <classifier>${escapeXml(sv.classifier)}</classifier>\n`;
}
xml += ` <extension>${escapeXml(sv.extension)}</extension>\n`;
xml += ` <value>${escapeXml(sv.value)}</value>\n`;
xml += ` <updated>${sv.updated}</updated>\n`;
xml += ' </snapshotVersion>\n';
}
xml += ' </snapshotVersions>\n';
}
xml += ' </versioning>\n';
xml += '</metadata>\n';
return xml;
}
/**
* Parse maven-metadata.xml to metadata object
* Basic XML parsing for Maven metadata
*/
export function parseMetadataXml(xml: string): IMavenMetadata | null {
try {
// Simple regex-based parsing (for basic metadata)
// In production, use a proper XML parser
const groupIdMatch = xml.match(/<groupId>([^<]+)<\/groupId>/);
const artifactIdMatch = xml.match(/<artifactId>([^<]+)<\/artifactId>/);
const latestMatch = xml.match(/<latest>([^<]+)<\/latest>/);
const releaseMatch = xml.match(/<release>([^<]+)<\/release>/);
const lastUpdatedMatch = xml.match(/<lastUpdated>([^<]+)<\/lastUpdated>/);
if (!groupIdMatch || !artifactIdMatch) {
return null;
}
// Parse versions
const versionsMatch = xml.match(/<versions>([\s\S]*?)<\/versions>/);
const versions: string[] = [];
if (versionsMatch) {
const versionMatches = versionsMatch[1].matchAll(/<version>([^<]+)<\/version>/g);
for (const match of versionMatches) {
versions.push(match[1]);
}
}
return {
groupId: groupIdMatch[1],
artifactId: artifactIdMatch[1],
versioning: {
latest: latestMatch ? latestMatch[1] : undefined,
release: releaseMatch ? releaseMatch[1] : undefined,
versions,
lastUpdated: lastUpdatedMatch ? lastUpdatedMatch[1] : formatMavenTimestamp(new Date()),
},
};
} catch (error) {
return null;
}
}
/**
* Escape XML special characters
*/
function escapeXml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
/**
* Format timestamp in Maven format: yyyyMMddHHmmss
*/
export function formatMavenTimestamp(date: Date): string {
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
return `${year}${month}${day}${hours}${minutes}${seconds}`;
}
/**
* Format SNAPSHOT timestamp: yyyyMMdd.HHmmss
*/
export function formatSnapshotTimestamp(date: Date): string {
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
return `${year}${month}${day}.${hours}${minutes}${seconds}`;
}
/**
* Check if version is a SNAPSHOT
*/
export function isSnapshot(version: string): boolean {
return version.endsWith('-SNAPSHOT');
}
/**
* Validate POM basic structure
*/
export function validatePom(pomXml: string): boolean {
try {
// Basic validation - check for required fields
return (
pomXml.includes('<groupId>') &&
pomXml.includes('<artifactId>') &&
pomXml.includes('<version>') &&
pomXml.includes('<modelVersion>')
);
} catch (error) {
return false;
}
}
/**
* Extract GAV from POM XML
*/
export function extractGAVFromPom(pomXml: string): { groupId: string; artifactId: string; version: string } | null {
try {
const groupIdMatch = pomXml.match(/<groupId>([^<]+)<\/groupId>/);
const artifactIdMatch = pomXml.match(/<artifactId>([^<]+)<\/artifactId>/);
const versionMatch = pomXml.match(/<version>([^<]+)<\/version>/);
if (groupIdMatch && artifactIdMatch && versionMatch) {
return {
groupId: groupIdMatch[1],
artifactId: artifactIdMatch[1],
version: versionMatch[1],
};
}
return null;
} catch (error) {
return null;
}
}