334 lines
9.4 KiB
TypeScript
334 lines
9.4 KiB
TypeScript
|
|
/**
|
||
|
|
* 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'}
|
||
|
|
*/
|
||
|
|
export function parseFilename(
|
||
|
|
filename: string,
|
||
|
|
artifactId: string,
|
||
|
|
version: string
|
||
|
|
): { classifier?: string; extension: string } | null {
|
||
|
|
// Expected format: {artifactId}-{version}[-{classifier}].{extension}
|
||
|
|
const prefix = `${artifactId}-${version}`;
|
||
|
|
|
||
|
|
if (!filename.startsWith(prefix)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const remainder = filename.substring(prefix.length);
|
||
|
|
|
||
|
|
// Check for classifier
|
||
|
|
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, '&')
|
||
|
|
.replace(/</g, '<')
|
||
|
|
.replace(/>/g, '>')
|
||
|
|
.replace(/"/g, '"')
|
||
|
|
.replace(/'/g, ''');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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;
|
||
|
|
}
|
||
|
|
}
|