From d2dbbf0538d91ee827e916bf4ff8d9b6d4a5aaeb Mon Sep 17 00:00:00 2001 From: Javier Quintero Date: Tue, 8 Oct 2024 14:29:34 -0500 Subject: [PATCH 1/4] Add Maven endpoint --- src/cfml/system/endpoints/Maven.cfc | 416 +++++++++++++++++++ src/cfml/system/services/EndpointService.cfc | 8 + 2 files changed, 424 insertions(+) create mode 100644 src/cfml/system/endpoints/Maven.cfc diff --git a/src/cfml/system/endpoints/Maven.cfc b/src/cfml/system/endpoints/Maven.cfc new file mode 100644 index 000000000..db46ee92c --- /dev/null +++ b/src/cfml/system/endpoints/Maven.cfc @@ -0,0 +1,416 @@ +/** +********************************************************************************* +* Copyright Since 2014 CommandBox by Ortus Solutions, Corp +* www.coldbox.org | www.ortussolutions.com +******************************************************************************** +* @author Brad Wood, Luis Majano, Denny Valliant +* +* I am the maven endpoint. I get packages from the maven repository +*/ +component accessors="true" implements="IEndpoint" singleton { + + // DI + property name="tempDir" inject="tempDir@constants"; + property name="semanticVersion" inject="provider:semanticVersion@semver"; + property name="progressableDownloader" inject="ProgressableDownloader"; + property name="progressBar" inject="ProgressBar"; + property name='JSONService' inject='JSONService'; + property name='configService' inject='configService'; + property name='wirebox' inject='wirebox'; + + // Properties + property name="namePrefixes" type="string"; + property name="repositoryBaseURL" type="string"; + + // Constructor + function init() { + setNamePrefixes( 'maven' ); + setRepositoryBaseURL( "https://maven-central.storage.googleapis.com/maven2/" ); + variables.defaultVersion = '0.0.0'; + return this; + } + + /** + * Resolves the Maven package based on the provided package string. + * Handles different URL patterns for Maven repositories. + * @package The package to resolve + * @currentWorkingDirectory The directory to resolve the package in + * @verbose Verbose flag or silent, defaults to false + */ + public string function resolvePackage( required string package, string currentWorkingDirectory="", boolean verbose=false ) { + if( configService.getSetting( 'offlineMode', false ) ) { + throw( 'Can''t download [#getNamePrefixes()#:#package#], CommandBox is in offline mode. Go online with [config set offlineMode=false].', 'endpointException' ); + } + + var job = wirebox.getInstance( 'interactiveJob' ); + var packageParts = getPackageParts( package ); + var jarFileURL = ""; + + // get artifact metadata to make sure it exists + try { + var artifactMetadata = getArtifactMetadata(packageParts.groupId, packageParts.artifactId,packageParts.repoURL); + } catch(Any e) { + throw( 'Could not find artifact metadata for [#packageParts.groupId#:#packageParts.artifactId#] in repository [#packageParts.repoURL#]', 'endpointException', e.detail ); + } + + // Get latest version if not specified + if( packageParts.version eq "LATEST" ) { + latestVersion = getLatestVersion(packageParts.groupId, packageParts.artifactId, packageParts.repoURL); + jarFileURL = getJarFileURL(packageParts.groupId, packageParts.artifactId, latestVersion); + packageParts.version = latestVersion; + } else { + // Get artifact version + jarFileURL = getJarFileURL(packageParts.groupId, packageParts.artifactId, packageParts.version); + } + + var folderName = tempDir & '/' & 'temp#createUUID()#'; + var fullJarPath = folderName & '/' & getDefaultName( package ) & '.jar'; + var fullBoxJSONPath = folderName & '/box.json'; + directoryCreate( folderName ); + + job.addLog( "Downloading [#packageParts.artifactId#]" ); + + try { + // Download File + var result = progressableDownloader.download( + jarFileURL, // URL to package + fullJarPath, // Place to store it locally + function( status ) { + progressBar.update( argumentCollection = status ); + }, + function( newURL ) { + job.addLog( "Redirecting to: '#arguments.newURL#'..." ); + } + ); + } catch( UserInterruptException var e ) { + directoryDelete( folderName, true ); + rethrow; + } catch( Any var e ) { + directoryDelete( folderName, true ); + throw( '#e.message##e.detail#', 'endpointException' ); + }; + + // Spoof a box.json so this looks like a package + var boxJSON = { + 'name' : '#packageParts.artifactId#.jar', + 'slug' : packageParts.artifactId, + 'version' : packageParts.version, + 'location' : package, + 'type' : 'jars' + }; + + JSONService.writeJSONFile( fullBoxJSONPath, boxJSON ); + + // Here is where our alleged so-called "package" lives. + return folderName; + } + + /** + * Get the default name of a package + * @package The package to get the default name for + */ + public function getDefaultName( required string package ) { + // example package string: "maven:https://repo1.maven.com##com.ortusolutions:myPackage:1.2.3-alpha"; + var packageParts = getPackageParts( package ); + + if( packageParts.artifactId.len() ) { + return packageParts.artifactId; + } + + return reReplaceNoCase( arguments.package, '[^a-zA-Z0-9]', '', 'all' ); + } + + /** + * Get an update for a package + * @package The package name + * @version The package version + * @verbose Verbose flag or silent, defaults to false + * + * @return struct { isOutdated, version } + */ + public function getUpdate( required string package, required string version, boolean verbose=false ) { + // TODO: Review this logic and use semver for version comparison + packageVersion = guessVersionFromURL( package ); + // No version could be determined from package URL + if( packageVersion == defaultVersion ) { + return { + isOutdated = true, + version = 'unknown' + }; + // Our package URL has a version and it's the same as what's installed + } else if( version == getLatestVersion() ) { + return { + isOutdated = false, + version = getLatestVersion() + }; + // our package URL has a version and it's not what's installed + } else { + return { + isOutdated = true, + version = getLatestVersion() + }; + } + } + + // Helper function to get the latest version of an artifact + private function getLatestVersion(string groupId, string artifactId, string repoURL = getRepositoryBaseURL()) { + var metadata = getArtifactMetadata(groupId, artifactId, repoURL); + + if( metadata.keyExists( "versioning" ) && metadata.versioning.keyExists( "latest" ) ) { + return metadata.versioning.latest; + } else { + return "unknown"; + } + } + + // Helper function to get the parts of a package string + private function getPackageParts(string package) { + var response = { + "repoURL": getRepositoryBaseURL(), + "groupId": "", + "artifactId": "", + "version": "" + }; + + // Remove the 'maven:' prefix from the package + package = replace(package, "maven:", "", "one"); + + // Split the package string by '#' to separate the repo and package + var parts = package.split("##"); + + // Determine if a custom repo is provided + if (arrayLen(parts) == 2) { + response.repoURL = parts[1]; // Use custom repo URL + package = parts[2]; // The actual package + } + + // Split the package into its components + var packageParts = package.split(":"); + + // Make sure we have at least a groupId and artifactId + if( arrayLen(packageParts) < 2 ) { + throw( 'Invalid Maven package string: #package#' ); + } else { + response.groupId = packageParts[1]; + response.artifactId = packageParts[2]; + response.version = packageParts[3] ?: "LATEST"; // Default to LATEST if not provided + } + + return response; + } + + // Helper function to get the parts of a package string + private function guessVersionFromURL( required string package ) { + var version = package; + if( version contains '/' ) { + var version = version + .reReplaceNoCase( '^([\w:]+)?//', '' ) + .listRest( '/\' ); + } + if( version.refindNoCase( '.*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*' ) ) { + version = version.reReplaceNoCase( '.*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*', '\1' ); + } else { + version = defaultVersion; + } + return version; + } + + // Helper function to get the artifact metadata + private function getArtifactMetadata(groupId, artifactId, repoURL = getRepositoryBaseURL() ) { + var addr = repoURL & replace(groupId, ".", "/", "ALL") & "/" & artifactId & "/"; + var httpResult = ""; + var metaData = ""; + var md = {"groupId":"", "artifactId":"", "versioning": {"latest":"", "release":"", "versions":[], "lastUpdated":""}}; + cfhttp(url="#addr#maven-metadata.xml", method="get", redirect=true, result="httpResult"); + if (httpResult.statusCode contains "200"){ + if (isSafeXML(httpResult.fileContent)) { + metaData = xmlParse(httpResult.fileContent); + md.groupId = metaData.xmlRoot.groupId.XmlText; + md.artifactId = metaData.xmlRoot.artifactId.XmlText; + if (structKeyExists(metaData.xmlRoot, "versioning")) { + md.versioning.latest = metaData.xmlRoot.versioning.latest.XmlText; + md.versioning.release = metaData.xmlRoot.versioning.release.XmlText; + for (local.version in metaData.xmlRoot.versioning.versions.XmlChildren) { + arrayAppend(md.versioning.versions, local.version.XmlText); + } + } + } else { + throw(message="Metadata XML Contained Potentially Unsafe Directives"); + } + + } else { + throw(message="Repository Request to #addr# returned status: #httpResult.statusCode#"); + } + return md; + } + + // Helper function to get the artifact version + private function getArtifactVersion(groupId, artifactId, version) { + var addr = getRepositoryBaseURL() & replace(groupId, ".", "/", "ALL") & "/" & artifactId & "/" & version & "/" & artifactId & "-" & version & ".pom"; + var httpResult = ""; + + cfhttp(url="#addr#", method="get", redirect=true, result="httpResult"); + if (httpResult.statusCode contains "200"){ + return parsePOM(httpResult.fileContent); + } else { + throw(message="Repository Request to #addr# returned status: #httpResult.statusCode#"); + } + } + + // Helper function to get the artifact and dependency jar URLs + private function getArtifactAndDependencyJarURLs(groupId, artifactId, version, scopes="runtime,compile", depth=0) { + var meta = getArtifactVersion(groupId, artifactId, version); + var cache = {}; + var result = []; + var dep = ""; + var d = ""; + var v = ""; + if (meta.packaging IS "jar") { + result = [{"download":getJarFileURL(groupId, artifactId, version), "groupId":arguments.groupId, "artifactId":arguments.artifactId, "version":arguments.version}]; + } + for (dep in meta.dependencies) { + if (!listFindNoCase(arguments.scopes, dep.scope)) { + //skip + continue; + } + if (dep.optional) { + continue; + } + if (!cache.keyExists(dep.groupId & "/" & dep.artifactId)) { + d = getArtifactMetadata(dep.groupId, dep.artifactId); + if (len(dep.version)) { + d.wantedVersion = [dep.version]; + } + cache[dep.groupId & "/" & dep.artifactId] = d; + } else if (len(dep.version)) { + //add as a wanted version + arrayAppend(cache[dep.groupId & "/" & dep.artifactId].wantedVersion, dep.version); + } + } + + for (dep in cache) { + dep = cache[dep]; + if (!dep.keyExists("wantedVersion")) { + v = dep.versioning.release; + } else { + //todo pick highest version + v = dep.wantedVersion[1]; + } + if (dep.artifactId == arguments.artifactId && dep.groupId == arguments.groupId) { + continue; + } + if (meta.packaging IS "pom" && dep.scope IS "import") { + if (depth > 10) { + throw(message="Maximum depth of 10 reached"); + } + d = getArtifactAndDependencyJarURLs(dep.groupId, dep.artifactId, v, scopes, depth++); + for (v in d) { + if (!arrayFind(result, v)) { + arrayAppend(result, v); + } + } + } else { + arrayAppend(result,{"download":getJarFileURL(dep.groupId, dep.artifactId, v), "groupId":dep.groupId, "artifactId":dep.artifactId, "version":v}); + } + } + return result; + } + + // Helper function to get the jar file URL + private function getJarFileURL(groupId, artifactId, version) { + var addr = getRepositoryBaseURL() & replace(groupId, ".", "/", "ALL") & "/" & artifactId & "/" & version & "/" & artifactId & "-" & version & ".jar"; + return addr; + } + + // Helper function to parse the POM XML + public function parsePOM(xmlString) { + var pom = {"name":"", "packaging"="", "dependencies":[], "xml"={}}; + var xml = ""; + var dep = ""; + var d = ""; + if (isSafeXML(xmlString)) { + xml = xmlParse(xmlString); + if (xml.xmlRoot.keyExists("name")) { + pom.name = xml.xmlRoot.name.xmlText; + } + if (xml.xmlRoot.keyExists("packaging")) { + pom.packaging = xml.xmlRoot.packaging.xmlText; + } + pom.xml = xml; + if (xml.xmlRoot.keyExists("dependencies")) { + pom.dependencies = parseDependencies(xml, xml.xmlRoot.dependencies); + } + if (xml.xmlRoot.keyExists("dependencyManagement")) { + dep = parseDependencies(xml, xml.xmlRoot.dependencyManagement.dependencies); + if (arrayIsEmpty(pom.dependencies)) { + pom.dependencies = dep; + } else { + for (d in dep) { + arrayAppend(pom.dependencies, d); + } + } + } + } else { + throw(message="POM XML Contained Potentially Unsafe Directives"); + } + return pom; + } + + // Helper function to parse the dependencies + private function parseDependencies(rootXml, node) { + var dep = ""; + var d = ""; + var deps = []; + var prop = ""; + var p = ""; + //Default scope is compile: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html + for (dep in node.XmlChildren) { + d = {"groupId":"", "artifactId":"", "scope":"compile", "type":"", "version":"", "optional":false}; + d.groupId = dep.groupId.XmlText; + d.artifactId = dep.artifactId.xmlText; + if (dep.keyExists("version")) { + d.version = dep.version.xmlText; + if (d.version == "${project.version}") { + d.version = rootXml.XmlRoot.version.xmlText; + } else if (d.version contains "${" && rootXml.XmlRoot.keyExists("properties")) { + //check properties ${prop.name} + for (prop in rootXml.XmlRoot.properties.XmlChildren) { + if (find("${" & prop.XmlName & "}", d.version)) { + d.version = replace(d.version, "${" & prop.XmlName & "}", prop.xmlText); + } + } + } + } + if (dep.keyExists("scope")) { + d.scope = dep.scope.xmlText; + } + if (dep.keyExists("type")) { + d.type = dep.type.xmlText; + } + if (dep.keyExists("optional")) { + d.optional = dep.optional.xmlText; + } + arrayAppend(deps, d); + } + return deps; + } + + // Helper function to determine if XML is safe + private function isSafeXML(xml) { + if (findNoCase("!doctype", arguments.xml)) { + return false; + } + if (findNoCase("!entity", arguments.xml)) { + return false; + } + if (findNoCase("!element", arguments.xml)) { + return false; + } + if (find("XInclude", arguments.xml)) { + return false; + } + //may be safe + return true; + } + +} diff --git a/src/cfml/system/services/EndpointService.cfc b/src/cfml/system/services/EndpointService.cfc index 88655db61..d25427013 100644 --- a/src/cfml/system/services/EndpointService.cfc +++ b/src/cfml/system/services/EndpointService.cfc @@ -150,6 +150,14 @@ component accessors="true" singleton { package : arguments.ID, ID : endpointName & ':' & arguments.ID }; + // Is it a maven package? + } else if( findNoCase( 'maven:', arguments.ID ) ) { + var endpointName = 'maven'; + return { + endpointName : endpointName, + package : arguments.ID, + ID : endpointName & ':' & arguments.ID + }; // Endpoint is specified as "endpoint:resource" } else if( listLen( arguments.ID, ':' ) > 1 ) { var endpointName = listFirst( arguments.ID, ':' ); From 8a7d708872be148fd12727c314a8088fc364ddec Mon Sep 17 00:00:00 2001 From: Javier Quintero Date: Tue, 15 Oct 2024 11:55:20 -0500 Subject: [PATCH 2/4] Add local artifacts logic and code format --- src/cfml/system/endpoints/Maven.cfc | 581 ++++++++++++------- src/cfml/system/services/EndpointService.cfc | 8 - 2 files changed, 358 insertions(+), 231 deletions(-) diff --git a/src/cfml/system/endpoints/Maven.cfc b/src/cfml/system/endpoints/Maven.cfc index db46ee92c..2ea8db8b5 100644 --- a/src/cfml/system/endpoints/Maven.cfc +++ b/src/cfml/system/endpoints/Maven.cfc @@ -1,105 +1,149 @@ /** -********************************************************************************* -* Copyright Since 2014 CommandBox by Ortus Solutions, Corp -* www.coldbox.org | www.ortussolutions.com -******************************************************************************** -* @author Brad Wood, Luis Majano, Denny Valliant -* -* I am the maven endpoint. I get packages from the maven repository -*/ -component accessors="true" implements="IEndpoint" singleton { + ********************************************************************************* + * Copyright Since 2014 CommandBox by Ortus Solutions, Corp + * www.coldbox.org | www.ortussolutions.com + ******************************************************************************** + * @author Brad Wood, Luis Majano, Denny Valliant + * + * I am the maven endpoint. I get packages from the maven repository + */ +component + accessors ="true" + implements="IEndpoint" + singleton +{ // DI - property name="tempDir" inject="tempDir@constants"; - property name="semanticVersion" inject="provider:semanticVersion@semver"; - property name="progressableDownloader" inject="ProgressableDownloader"; - property name="progressBar" inject="ProgressBar"; - property name='JSONService' inject='JSONService'; - property name='configService' inject='configService'; - property name='wirebox' inject='wirebox'; + property name="jarEndpoint" inject="commandbox.system.endpoints.Jar"; + property name="artifactService" inject="ArtifactService"; + // property name="semanticVersion" inject="provider:semanticVersion@semver"; + property name="JSONService" inject="JSONService"; + property name="configService" inject="configService"; + property name="wirebox" inject="wirebox"; // Properties - property name="namePrefixes" type="string"; + property name="namePrefixes" type="string"; property name="repositoryBaseURL" type="string"; // Constructor - function init() { - setNamePrefixes( 'maven' ); + function init(){ + setNamePrefixes( "maven" ); setRepositoryBaseURL( "https://maven-central.storage.googleapis.com/maven2/" ); - variables.defaultVersion = '0.0.0'; + variables.defaultVersion = "0.0.0"; return this; } /** - * Resolves the Maven package based on the provided package string. - * Handles different URL patterns for Maven repositories. + * Resolves the Maven package based on the provided package string. + * Handles different URL patterns for Maven repositories. * @package The package to resolve * @currentWorkingDirectory The directory to resolve the package in * @verbose Verbose flag or silent, defaults to false - */ - public string function resolvePackage( required string package, string currentWorkingDirectory="", boolean verbose=false ) { - if( configService.getSetting( 'offlineMode', false ) ) { - throw( 'Can''t download [#getNamePrefixes()#:#package#], CommandBox is in offline mode. Go online with [config set offlineMode=false].', 'endpointException' ); - } - - var job = wirebox.getInstance( 'interactiveJob' ); + */ + public string function resolvePackage( + required string package, + string currentWorkingDirectory = "", + boolean verbose = false + ){ + var job = wirebox.getInstance( "interactiveJob" ); var packageParts = getPackageParts( package ); - var jarFileURL = ""; + var jarFileURL = ""; + + // If the local artifact exists, serve it + if ( + artifactService.artifactExists( packageParts.artifactId, packageParts.version ) && packageParts.version != "LATEST" + ) { + job.addLog( "Lucky you, we found this version in local artifacts!" ); + var thisArtifactPath = artifactService.getArtifactPath( packageParts.artifactId, packageParts.version ); + + // Return folder path + return getDirectoryFromPath( thisArtifactPath ); + } - // get artifact metadata to make sure it exists + // get artifact metadata to make sure it exists try { - var artifactMetadata = getArtifactMetadata(packageParts.groupId, packageParts.artifactId,packageParts.repoURL); - } catch(Any e) { - throw( 'Could not find artifact metadata for [#packageParts.groupId#:#packageParts.artifactId#] in repository [#packageParts.repoURL#]', 'endpointException', e.detail ); + var artifactMetadata = getArtifactMetadata( + packageParts.groupId, + packageParts.artifactId, + packageParts.repoURL + ); + } catch ( Any e ) { + throw( + "Could not find artifact metadata for [#packageParts.groupId#:#packageParts.artifactId#] in repository [#packageParts.repoURL#]", + "endpointException", + e.detail + ); } // Get latest version if not specified - if( packageParts.version eq "LATEST" ) { - latestVersion = getLatestVersion(packageParts.groupId, packageParts.artifactId, packageParts.repoURL); - jarFileURL = getJarFileURL(packageParts.groupId, packageParts.artifactId, latestVersion); + if ( packageParts.version eq "LATEST" ) { + latestVersion = getLatestVersion( + packageParts.groupId, + packageParts.artifactId, + packageParts.repoURL + ); + jarFileURL = getJarFileURL( + packageParts.groupId, + packageParts.artifactId, + latestVersion + ); packageParts.version = latestVersion; } else { - // Get artifact version - jarFileURL = getJarFileURL(packageParts.groupId, packageParts.artifactId, packageParts.version); + // Get artifact for the passed in version + jarFileURL = getJarFileURL( + packageParts.groupId, + packageParts.artifactId, + packageParts.version + ); } - var folderName = tempDir & '/' & 'temp#createUUID()#'; - var fullJarPath = folderName & '/' & getDefaultName( package ) & '.jar'; - var fullBoxJSONPath = folderName & '/box.json'; - directoryCreate( folderName ); - - job.addLog( "Downloading [#packageParts.artifactId#]" ); - - try { - // Download File - var result = progressableDownloader.download( - jarFileURL, // URL to package - fullJarPath, // Place to store it locally - function( status ) { - progressBar.update( argumentCollection = status ); - }, - function( newURL ) { - job.addLog( "Redirecting to: '#arguments.newURL#'..." ); - } - ); - } catch( UserInterruptException var e ) { - directoryDelete( folderName, true ); - rethrow; - } catch( Any var e ) { - directoryDelete( folderName, true ); - throw( '#e.message##e.detail#', 'endpointException' ); - }; + // Defer to jar endpoint + var folderName = jarEndpoint.resolvePackage( + jarFileURL, + currentWorkingDirectory, + arguments.verbose + ); + + job.addLog( "Storing download in artifact cache..." ); + + // store it locally in the artifact cache + artifactService.createArtifact( + packageParts.artifactId, + packageParts.version, + folderName + ); + + job.addLog( "Done." ); + + // get dependencies + var artifactDependencies = getArtifactAndDependencyJarURLs( + packageParts.groupId, + packageParts.artifactId, + packageParts.version + ); + var installPaths = {}; + var dependencies = {}; + + for ( var dependency in artifactDependencies ) { + if ( dependency.artifactId == packageParts.artifactId ) { + continue; + } + dependencies[ dependency.artifactId ] = dependency.download; + installPaths[ dependency.artifactId ] = "lib/" & dependency.artifactId; + } - // Spoof a box.json so this looks like a package + // override the box.json with the actual version and dependencies var boxJSON = { - 'name' : '#packageParts.artifactId#.jar', - 'slug' : packageParts.artifactId, - 'version' : packageParts.version, - 'location' : package, - 'type' : 'jars' + "name" : "#packageParts.artifactId#.jar", + "slug" : packageParts.artifactId, + "version" : packageParts.version, + "location" : package, + "type" : "jars", + "dependencies" : dependencies, + "installPaths" : installPaths }; - JSONService.writeJSONFile( fullBoxJSONPath, boxJSON ); + JSONService.writeJSONFile( folderName & "/box.json", boxJSON ); // Here is where our alleged so-called "package" lives. return folderName; @@ -109,15 +153,19 @@ component accessors="true" implements="IEndpoint" singleton { * Get the default name of a package * @package The package to get the default name for */ - public function getDefaultName( required string package ) { - // example package string: "maven:https://repo1.maven.com##com.ortusolutions:myPackage:1.2.3-alpha"; + public function getDefaultName( required string package ){ var packageParts = getPackageParts( package ); - if( packageParts.artifactId.len() ) { + if ( packageParts.artifactId.len() ) { return packageParts.artifactId; } - return reReplaceNoCase( arguments.package, '[^a-zA-Z0-9]', '', 'all' ); + return reReplaceNoCase( + arguments.package, + "[^a-zA-Z0-9]", + "", + "all" + ); } /** @@ -128,87 +176,90 @@ component accessors="true" implements="IEndpoint" singleton { * * @return struct { isOutdated, version } */ - public function getUpdate( required string package, required string version, boolean verbose=false ) { - // TODO: Review this logic and use semver for version comparison + public function getUpdate( + required string package, + required string version, + boolean verbose = false + ){ + // Review this logic and use semver for version comparison packageVersion = guessVersionFromURL( package ); // No version could be determined from package URL - if( packageVersion == defaultVersion ) { + if ( packageVersion == defaultVersion ) { return { - isOutdated = true, - version = 'unknown' + isOutdated : true, + version : "unknown" }; - // Our package URL has a version and it's the same as what's installed - } else if( version == getLatestVersion() ) { + // Our package URL has a version and it's the same as what's installed + } else if ( packageVersion == version ) { return { - isOutdated = false, - version = getLatestVersion() + isOutdated : false, + version : version }; - // our package URL has a version and it's not what's installed + // our package URL has a version and it's not what's installed } else { - return { - isOutdated = true, - version = getLatestVersion() - }; + return { isOutdated : true, version : version }; } } // Helper function to get the latest version of an artifact - private function getLatestVersion(string groupId, string artifactId, string repoURL = getRepositoryBaseURL()) { - var metadata = getArtifactMetadata(groupId, artifactId, repoURL); - - if( metadata.keyExists( "versioning" ) && metadata.versioning.keyExists( "latest" ) ) { + private function getLatestVersion( + string groupId, + string artifactId, + string repoURL = getRepositoryBaseURL() + ){ + var metadata = getArtifactMetadata( groupId, artifactId, repoURL ); + + if ( metadata.keyExists( "versioning" ) && metadata.versioning.keyExists( "latest" ) ) { return metadata.versioning.latest; } else { return "unknown"; } - } + } // Helper function to get the parts of a package string - private function getPackageParts(string package) { + private function getPackageParts( string package ){ var response = { - "repoURL": getRepositoryBaseURL(), - "groupId": "", - "artifactId": "", - "version": "" + "repoURL" : getRepositoryBaseURL(), + "groupId" : "", + "artifactId" : "", + "version" : "" }; // Remove the 'maven:' prefix from the package - package = replace(package, "maven:", "", "one"); + package = replace( package, "maven:", "", "one" ); + + // Split the package string by '|' to separate the repo and package + var parts = package.split( "|" ); - // Split the package string by '#' to separate the repo and package - var parts = package.split("##"); - - // Determine if a custom repo is provided - if (arrayLen(parts) == 2) { - response.repoURL = parts[1]; // Use custom repo URL - package = parts[2]; // The actual package - } + // Determine if a custom repo is provided + if ( arrayLen( parts ) == 2 ) { + response.repoURL = parts[ 1 ]; // Use custom repo URL + package = parts[ 2 ]; // The actual package + } // Split the package into its components - var packageParts = package.split(":"); + var packageParts = package.split( ":" ); // Make sure we have at least a groupId and artifactId - if( arrayLen(packageParts) < 2 ) { - throw( 'Invalid Maven package string: #package#' ); + if ( arrayLen( packageParts ) < 2 ) { + throw( "Invalid Maven package string: #package#" ); } else { - response.groupId = packageParts[1]; - response.artifactId = packageParts[2]; - response.version = packageParts[3] ?: "LATEST"; // Default to LATEST if not provided + response.groupId = packageParts[ 1 ]; + response.artifactId = packageParts[ 2 ]; + response.version = packageParts[ 3 ] ?: "LATEST"; // Default to LATEST if not provided } return response; - } + } // Helper function to get the parts of a package string - private function guessVersionFromURL( required string package ) { + private function guessVersionFromURL( required string package ){ var version = package; - if( version contains '/' ) { - var version = version - .reReplaceNoCase( '^([\w:]+)?//', '' ) - .listRest( '/\' ); + if ( version contains "/" ) { + var version = version.reReplaceNoCase( "^([\w:]+)?//", "" ).listRest( "/\" ); } - if( version.refindNoCase( '.*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*' ) ) { - version = version.reReplaceNoCase( '.*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*', '\1' ); + if ( version.refindNoCase( ".*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*" ) ) { + version = version.reReplaceNoCase( ".*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*", "\1" ); } else { version = defaultVersion; } @@ -216,200 +267,284 @@ component accessors="true" implements="IEndpoint" singleton { } // Helper function to get the artifact metadata - private function getArtifactMetadata(groupId, artifactId, repoURL = getRepositoryBaseURL() ) { - var addr = repoURL & replace(groupId, ".", "/", "ALL") & "/" & artifactId & "/"; + private function getArtifactMetadata( + groupId, + artifactId, + repoURL = getRepositoryBaseURL() + ){ + var addr = repoURL & replace( groupId, ".", "/", "ALL" ) & "/" & artifactId & "/"; var httpResult = ""; - var metaData = ""; - var md = {"groupId":"", "artifactId":"", "versioning": {"latest":"", "release":"", "versions":[], "lastUpdated":""}}; - cfhttp(url="#addr#maven-metadata.xml", method="get", redirect=true, result="httpResult"); - if (httpResult.statusCode contains "200"){ - if (isSafeXML(httpResult.fileContent)) { - metaData = xmlParse(httpResult.fileContent); - md.groupId = metaData.xmlRoot.groupId.XmlText; + var metaData = ""; + var md = { + "groupId" : "", + "artifactId" : "", + "versioning" : { + "latest" : "", + "release" : "", + "versions" : [], + "lastUpdated" : "" + } + }; + + if ( configService.getSetting( "offlineMode", false ) ) { + throw( + "Can't download [#getNamePrefixes()#:#artifactId#], CommandBox is in offline mode. Go online with [config set offlineMode=false].", + "endpointException" + ); + } + cfhttp( + url = "#addr#maven-metadata.xml", + proxyServer = "#configService.getSetting( "proxy.server", "" )#", + method = "get", + redirect = true, + result = "httpResult" + ); + if ( httpResult.statusCode contains "200" ) { + if ( isSafeXML( httpResult.fileContent ) ) { + metaData = xmlParse( httpResult.fileContent ); + md.groupId = metaData.xmlRoot.groupId.XmlText; md.artifactId = metaData.xmlRoot.artifactId.XmlText; - if (structKeyExists(metaData.xmlRoot, "versioning")) { - md.versioning.latest = metaData.xmlRoot.versioning.latest.XmlText; + if ( structKeyExists( metaData.xmlRoot, "versioning" ) ) { + md.versioning.latest = metaData.xmlRoot.versioning.latest.XmlText; md.versioning.release = metaData.xmlRoot.versioning.release.XmlText; - for (local.version in metaData.xmlRoot.versioning.versions.XmlChildren) { - arrayAppend(md.versioning.versions, local.version.XmlText); + for ( local.version in metaData.xmlRoot.versioning.versions.XmlChildren ) { + arrayAppend( md.versioning.versions, local.version.XmlText ); } } } else { - throw(message="Metadata XML Contained Potentially Unsafe Directives"); + throw( message = "Metadata XML Contained Potentially Unsafe Directives" ); } - } else { - throw(message="Repository Request to #addr# returned status: #httpResult.statusCode#"); + throw( message = "Repository Request to #addr# returned status: #httpResult.statusCode#" ); } return md; } // Helper function to get the artifact version - private function getArtifactVersion(groupId, artifactId, version) { - var addr = getRepositoryBaseURL() & replace(groupId, ".", "/", "ALL") & "/" & artifactId & "/" & version & "/" & artifactId & "-" & version & ".pom"; + private function getArtifactVersion( groupId, artifactId, version ){ + var addr = getRepositoryBaseURL() & replace( groupId, ".", "/", "ALL" ) & "/" & artifactId & "/" & version & "/" & artifactId & "-" & version & ".pom"; var httpResult = ""; - - cfhttp(url="#addr#", method="get", redirect=true, result="httpResult"); - if (httpResult.statusCode contains "200"){ - return parsePOM(httpResult.fileContent); + + var job = wirebox.getInstance( "interactiveJob" ); + job.addLog( "Address: [#addr#]" ); + + if ( configService.getSetting( "offlineMode", false ) ) { + throw( + "Can't download [#getNamePrefixes()#:#artifactId#], CommandBox is in offline mode. Go online with [config set offlineMode=false].", + "endpointException" + ); + } + + cfhttp( + url = "#addr#", + proxyServer = "#configService.getSetting( "proxy.server", "" )#", + method = "get", + redirect = true, + result = "httpResult" + ); + if ( httpResult.statusCode contains "200" ) { + return parsePOM( httpResult.fileContent ); } else { - throw(message="Repository Request to #addr# returned status: #httpResult.statusCode#"); + throw( message = "Repository Request to #addr# returned status: #httpResult.statusCode#" ); } } // Helper function to get the artifact and dependency jar URLs - private function getArtifactAndDependencyJarURLs(groupId, artifactId, version, scopes="runtime,compile", depth=0) { - var meta = getArtifactVersion(groupId, artifactId, version); - var cache = {}; + private function getArtifactAndDependencyJarURLs( + groupId, + artifactId, + version, + scopes = "runtime,compile", + depth = 0 + ){ + var meta = getArtifactVersion( groupId, artifactId, version ); + var cache = {}; var result = []; - var dep = ""; - var d = ""; - var v = ""; - if (meta.packaging IS "jar") { - result = [{"download":getJarFileURL(groupId, artifactId, version), "groupId":arguments.groupId, "artifactId":arguments.artifactId, "version":arguments.version}]; + var dep = ""; + var d = ""; + var v = ""; + if ( meta.packaging IS "jar" ) { + result = [ + { + "download" : getJarFileURL( groupId, artifactId, version ), + "groupId" : arguments.groupId, + "artifactId" : arguments.artifactId, + "version" : arguments.version + } + ]; } - for (dep in meta.dependencies) { - if (!listFindNoCase(arguments.scopes, dep.scope)) { - //skip + for ( dep in meta.dependencies ) { + if ( !listFindNoCase( arguments.scopes, dep.scope ) ) { + // skip continue; } - if (dep.optional) { + if ( dep.optional ) { continue; } - if (!cache.keyExists(dep.groupId & "/" & dep.artifactId)) { - d = getArtifactMetadata(dep.groupId, dep.artifactId); - if (len(dep.version)) { - d.wantedVersion = [dep.version]; + if ( !cache.keyExists( dep.groupId & "/" & dep.artifactId ) ) { + d = getArtifactMetadata( dep.groupId, dep.artifactId ); + if ( len( dep.version ) ) { + d.wantedVersion = [ dep.version ]; } - cache[dep.groupId & "/" & dep.artifactId] = d; - } else if (len(dep.version)) { - //add as a wanted version - arrayAppend(cache[dep.groupId & "/" & dep.artifactId].wantedVersion, dep.version); + cache[ dep.groupId & "/" & dep.artifactId ] = d; + } else if ( len( dep.version ) ) { + // add as a wanted version + arrayAppend( cache[ dep.groupId & "/" & dep.artifactId ].wantedVersion, dep.version ); } } - - for (dep in cache) { - dep = cache[dep]; - if (!dep.keyExists("wantedVersion")) { + + for ( dep in cache ) { + dep = cache[ dep ]; + if ( !dep.keyExists( "wantedVersion" ) ) { v = dep.versioning.release; } else { - //todo pick highest version - v = dep.wantedVersion[1]; + // todo pick highest version + v = dep.wantedVersion[ 1 ]; } - if (dep.artifactId == arguments.artifactId && dep.groupId == arguments.groupId) { + if ( dep.artifactId == arguments.artifactId && dep.groupId == arguments.groupId ) { continue; } - if (meta.packaging IS "pom" && dep.scope IS "import") { - if (depth > 10) { - throw(message="Maximum depth of 10 reached"); + if ( meta.packaging IS "pom" && dep.scope IS "import" ) { + if ( depth > 10 ) { + throw( message = "Maximum depth of 10 reached" ); } - d = getArtifactAndDependencyJarURLs(dep.groupId, dep.artifactId, v, scopes, depth++); - for (v in d) { - if (!arrayFind(result, v)) { - arrayAppend(result, v); + d = getArtifactAndDependencyJarURLs( + dep.groupId, + dep.artifactId, + v, + scopes, + depth++ + ); + for ( v in d ) { + if ( !arrayFind( result, v ) ) { + arrayAppend( result, v ); } } } else { - arrayAppend(result,{"download":getJarFileURL(dep.groupId, dep.artifactId, v), "groupId":dep.groupId, "artifactId":dep.artifactId, "version":v}); + arrayAppend( + result, + { + "download" : getJarFileURL( dep.groupId, dep.artifactId, v ), + "groupId" : dep.groupId, + "artifactId" : dep.artifactId, + "version" : v + } + ); } } return result; } // Helper function to get the jar file URL - private function getJarFileURL(groupId, artifactId, version) { - var addr = getRepositoryBaseURL() & replace(groupId, ".", "/", "ALL") & "/" & artifactId & "/" & version & "/" & artifactId & "-" & version & ".jar"; + private function getJarFileURL( groupId, artifactId, version ){ + var addr = getRepositoryBaseURL() & replace( groupId, ".", "/", "ALL" ) & "/" & artifactId & "/" & version & "/" & artifactId & "-" & version & ".jar"; return addr; } // Helper function to parse the POM XML - public function parsePOM(xmlString) { - var pom = {"name":"", "packaging"="", "dependencies":[], "xml"={}}; + public function parsePOM( xmlString ){ + var pom = { + "name" : "", + "packaging" : "", + "dependencies" : [], + "xml" : {} + }; var xml = ""; var dep = ""; - var d = ""; - if (isSafeXML(xmlString)) { - xml = xmlParse(xmlString); - if (xml.xmlRoot.keyExists("name")) { + var d = ""; + if ( isSafeXML( xmlString ) ) { + xml = xmlParse( xmlString ); + if ( xml.xmlRoot.keyExists( "name" ) ) { pom.name = xml.xmlRoot.name.xmlText; } - if (xml.xmlRoot.keyExists("packaging")) { + if ( xml.xmlRoot.keyExists( "packaging" ) ) { pom.packaging = xml.xmlRoot.packaging.xmlText; } pom.xml = xml; - if (xml.xmlRoot.keyExists("dependencies")) { - pom.dependencies = parseDependencies(xml, xml.xmlRoot.dependencies); + if ( xml.xmlRoot.keyExists( "dependencies" ) ) { + pom.dependencies = parseDependencies( xml, xml.xmlRoot.dependencies ); } - if (xml.xmlRoot.keyExists("dependencyManagement")) { - dep = parseDependencies(xml, xml.xmlRoot.dependencyManagement.dependencies); - if (arrayIsEmpty(pom.dependencies)) { + if ( xml.xmlRoot.keyExists( "dependencyManagement" ) ) { + dep = parseDependencies( xml, xml.xmlRoot.dependencyManagement.dependencies ); + if ( arrayIsEmpty( pom.dependencies ) ) { pom.dependencies = dep; } else { - for (d in dep) { - arrayAppend(pom.dependencies, d); + for ( d in dep ) { + arrayAppend( pom.dependencies, d ); } } } } else { - throw(message="POM XML Contained Potentially Unsafe Directives"); + throw( message = "POM XML Contained Potentially Unsafe Directives" ); } return pom; } // Helper function to parse the dependencies - private function parseDependencies(rootXml, node) { - var dep = ""; - var d = ""; + private function parseDependencies( rootXml, node ){ + var dep = ""; + var d = ""; var deps = []; var prop = ""; - var p = ""; - //Default scope is compile: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html - for (dep in node.XmlChildren) { - d = {"groupId":"", "artifactId":"", "scope":"compile", "type":"", "version":"", "optional":false}; - d.groupId = dep.groupId.XmlText; + var p = ""; + // Default scope is compile: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html + for ( dep in node.XmlChildren ) { + d = { + "groupId" : "", + "artifactId" : "", + "scope" : "compile", + "type" : "", + "version" : "", + "optional" : false + }; + d.groupId = dep.groupId.XmlText; d.artifactId = dep.artifactId.xmlText; - if (dep.keyExists("version")) { + if ( dep.keyExists( "version" ) ) { d.version = dep.version.xmlText; - if (d.version == "${project.version}") { + if ( d.version == "${project.version}" ) { d.version = rootXml.XmlRoot.version.xmlText; - } else if (d.version contains "${" && rootXml.XmlRoot.keyExists("properties")) { - //check properties ${prop.name} - for (prop in rootXml.XmlRoot.properties.XmlChildren) { - if (find("${" & prop.XmlName & "}", d.version)) { - d.version = replace(d.version, "${" & prop.XmlName & "}", prop.xmlText); + } else if ( d.version contains "${" && rootXml.XmlRoot.keyExists( "properties" ) ) { + // check properties ${prop.name} + for ( prop in rootXml.XmlRoot.properties.XmlChildren ) { + if ( find( "${" & prop.XmlName & "}", d.version ) ) { + d.version = replace( + d.version, + "${" & prop.XmlName & "}", + prop.xmlText + ); } } } } - if (dep.keyExists("scope")) { + if ( dep.keyExists( "scope" ) ) { d.scope = dep.scope.xmlText; } - if (dep.keyExists("type")) { + if ( dep.keyExists( "type" ) ) { d.type = dep.type.xmlText; } - if (dep.keyExists("optional")) { + if ( dep.keyExists( "optional" ) ) { d.optional = dep.optional.xmlText; } - arrayAppend(deps, d); + arrayAppend( deps, d ); } return deps; } // Helper function to determine if XML is safe - private function isSafeXML(xml) { - if (findNoCase("!doctype", arguments.xml)) { + private function isSafeXML( xml ){ + if ( findNoCase( "!doctype", arguments.xml ) ) { return false; } - if (findNoCase("!entity", arguments.xml)) { + if ( findNoCase( "!entity", arguments.xml ) ) { return false; } - if (findNoCase("!element", arguments.xml)) { + if ( findNoCase( "!element", arguments.xml ) ) { return false; } - if (find("XInclude", arguments.xml)) { + if ( find( "XInclude", arguments.xml ) ) { return false; } - //may be safe + // may be safe return true; } diff --git a/src/cfml/system/services/EndpointService.cfc b/src/cfml/system/services/EndpointService.cfc index d25427013..88655db61 100644 --- a/src/cfml/system/services/EndpointService.cfc +++ b/src/cfml/system/services/EndpointService.cfc @@ -150,14 +150,6 @@ component accessors="true" singleton { package : arguments.ID, ID : endpointName & ':' & arguments.ID }; - // Is it a maven package? - } else if( findNoCase( 'maven:', arguments.ID ) ) { - var endpointName = 'maven'; - return { - endpointName : endpointName, - package : arguments.ID, - ID : endpointName & ':' & arguments.ID - }; // Endpoint is specified as "endpoint:resource" } else if( listLen( arguments.ID, ':' ) > 1 ) { var endpointName = listFirst( arguments.ID, ':' ); From c13c0d39af548b6ea400003d3c48066812bec3eb Mon Sep 17 00:00:00 2001 From: Javier Quintero Date: Mon, 17 Feb 2025 09:57:23 -0500 Subject: [PATCH 3/4] Improve maven support and add version ranges --- src/cfml/system/endpoints/Maven.cfc | 428 +++++++++++++++++++--------- 1 file changed, 289 insertions(+), 139 deletions(-) diff --git a/src/cfml/system/endpoints/Maven.cfc b/src/cfml/system/endpoints/Maven.cfc index 2ea8db8b5..b753d8e99 100644 --- a/src/cfml/system/endpoints/Maven.cfc +++ b/src/cfml/system/endpoints/Maven.cfc @@ -5,7 +5,7 @@ ******************************************************************************** * @author Brad Wood, Luis Majano, Denny Valliant * - * I am the maven endpoint. I get packages from the maven repository + * I am the maven endpoint. I get artifacts from the maven repository */ component accessors ="true" @@ -15,21 +15,30 @@ component // DI property name="jarEndpoint" inject="commandbox.system.endpoints.Jar"; + property name="fileEndpoint" inject="commandbox.system.endpoints.File"; property name="artifactService" inject="ArtifactService"; - // property name="semanticVersion" inject="provider:semanticVersion@semver"; + property name="semanticVersion" inject="provider:semanticVersion@semver"; property name="JSONService" inject="JSONService"; property name="configService" inject="configService"; property name="wirebox" inject="wirebox"; // Properties - property name="namePrefixes" type="string"; - property name="repositoryBaseURL" type="string"; + property name="namePrefixes" type="string"; + property name="globalRepos" type="struct"; // Constructor function init(){ setNamePrefixes( "maven" ); - setRepositoryBaseURL( "https://maven-central.storage.googleapis.com/maven2/" ); - variables.defaultVersion = "0.0.0"; + var orderedStruct = structNew( "ordered" ); + orderedStruct[ "mavenCentral" ] = "https://maven-central.storage.googleapis.com/maven2/"; + orderedStruct[ "sonatype" ] = "https://oss.sonatype.org/content/repositories/releases/"; + orderedStruct[ "jitpack" ] = "https://jitpack.io/"; + orderedStruct[ "google" ] = "https://maven.google.com/"; + orderedStruct[ "spring" ] = "https://repo.spring.io/release/"; + orderedStruct[ "jboss" ] = "https://repository.jboss.org/nexus/content/repositories/releases/"; + orderedStruct[ "apacheSnapshots" ] = "https://repository.apache.org/snapshots/"; + orderedStruct[ "gradlePlugins" ] = "https://plugins.gradle.org/m2/"; + setGlobalRepos( orderedStruct ); return this; } @@ -45,99 +54,116 @@ component string currentWorkingDirectory = "", boolean verbose = false ){ - var job = wirebox.getInstance( "interactiveJob" ); - var packageParts = getPackageParts( package ); - var jarFileURL = ""; + var job = wirebox.getInstance( "interactiveJob" ); + var repos = getGlobalRepos(); // Global linked struct of repos + // var projectRepos = {}; // TODO: Local overrides from box.json + // var repos = structAppend(globalRepos, projectRepos, false); // Preserve order + + var artifactParts = getArtifactParts( package ); + var jarFileURL = ""; + var artifact = { + "jarFileURL" : "", + "artifactMetadata" : {} + }; // If the local artifact exists, serve it if ( - artifactService.artifactExists( packageParts.artifactId, packageParts.version ) && packageParts.version != "LATEST" + artifactService.artifactExists( artifactParts.artifactId, artifactParts.version ) && artifactParts.version != "STABLE" && !semanticVersion.isExactVersion( + artifactParts.version, + true + ) ) { job.addLog( "Lucky you, we found this version in local artifacts!" ); - var thisArtifactPath = artifactService.getArtifactPath( packageParts.artifactId, packageParts.version ); - - // Return folder path - return getDirectoryFromPath( thisArtifactPath ); - } + var thisArtifactPath = artifactService.getArtifactPath( artifactParts.artifactId, artifactParts.version ); - // get artifact metadata to make sure it exists - try { - var artifactMetadata = getArtifactMetadata( - packageParts.groupId, - packageParts.artifactId, - packageParts.repoURL - ); - } catch ( Any e ) { - throw( - "Could not find artifact metadata for [#packageParts.groupId#:#packageParts.artifactId#] in repository [#packageParts.repoURL#]", - "endpointException", - e.detail + // Return the path to the artifact + return fileEndpoint.resolvePackage( + thisArtifactPath, + currentWorkingDirectory, + arguments.verbose ); } - // Get latest version if not specified - if ( packageParts.version eq "LATEST" ) { - latestVersion = getLatestVersion( - packageParts.groupId, - packageParts.artifactId, - packageParts.repoURL - ); - jarFileURL = getJarFileURL( - packageParts.groupId, - packageParts.artifactId, - latestVersion - ); - packageParts.version = latestVersion; - } else { - // Get artifact for the passed in version - jarFileURL = getJarFileURL( - packageParts.groupId, - packageParts.artifactId, - packageParts.version + // Check only the explicitly defined repo, if any + if ( len( artifactParts.repo ) ) { + artifact = getArtifactFromRepo( + artifactParts.repo, + artifactParts.groupId, + artifactParts.artifactId, + artifactParts.version ); + jarFileURL = artifact.jarFileURL; + } + // Otherwise, check each registered repo sequentially + else { + for ( var alias in repos ) { + artifact = getArtifactFromRepo( + repos[ alias ], + artifactParts.groupId, + artifactParts.artifactId, + artifactParts.version + ); + jarFileURL = artifact.jarFileURL; + // If we found the artifact, break out of the loop + if ( jarFileURL.len() ) { + break; + } + } } // Defer to jar endpoint var folderName = jarEndpoint.resolvePackage( - jarFileURL, + artifact.jarFileURL, currentWorkingDirectory, arguments.verbose ); - job.addLog( "Storing download in artifact cache..." ); - - // store it locally in the artifact cache - artifactService.createArtifact( - packageParts.artifactId, - packageParts.version, - folderName - ); + if ( artifactParts.version eq "STABLE" ) { + artifactParts.version = getLatestVersion( artifactParts.groupId, artifactParts.artifactId ); + } - job.addLog( "Done." ); + // Update artifact version if it's a range + else if ( !semanticVersion.isExactVersion( artifactParts.version, true ) ) { + if ( + artifact.artifactMetadata.keyExists( "versioning" ) && artifact.artifactMetadata.versioning.keyExists( "versions" ) && artifact.artifactMetadata.versioning.versions.len() + ) { + var sortedVersions = artifact.artifactMetadata.versioning.versions.sort( ( a, b ) => variables.semanticVersion.compare( b, a ) ); + // Get the latest version that matches the range + for ( var thisVersion in sortedVersions ) { + if ( semanticVersion.satisfies( thisVersion, artifactParts.version ) ) { + artifactParts.version = thisVersion; + break; + } + } + } + } // get dependencies var artifactDependencies = getArtifactAndDependencyJarURLs( - packageParts.groupId, - packageParts.artifactId, - packageParts.version + artifactParts.groupId, + artifactParts.artifactId, + artifactParts.version ); + var installPaths = {}; var dependencies = {}; for ( var dependency in artifactDependencies ) { - if ( dependency.artifactId == packageParts.artifactId ) { + if ( dependency.artifactId == artifactParts.artifactId ) { continue; } - dependencies[ dependency.artifactId ] = dependency.download; + dependencies[ dependency.artifactId ] = getNamePrefixes() & ( + artifactParts.repo.len() ? artifactParts.repo & "|" : "" + ) & dependency.groupId & ":" & dependency.artifactId & ":" & dependency.version; installPaths[ dependency.artifactId ] = "lib/" & dependency.artifactId; } // override the box.json with the actual version and dependencies var boxJSON = { - "name" : "#packageParts.artifactId#.jar", - "slug" : packageParts.artifactId, - "version" : packageParts.version, - "location" : package, + "name" : "#artifactParts.groupId & "-" & artifactParts.artifactId#.jar", + "slug" : artifactParts.artifactId, + "version" : artifactParts.version, + "location" : "maven:" & arguments.package, "type" : "jars", "dependencies" : dependencies, "installPaths" : installPaths @@ -145,6 +171,17 @@ component JSONService.writeJSONFile( folderName & "/box.json", boxJSON ); + job.addLog( "Storing download in artifact cache..." ); + + // store it locally in the artifact cache + artifactService.createArtifact( + artifactParts.artifactId, + artifactParts.version, + folderName + ); + + job.addLog( "Done." ); + // Here is where our alleged so-called "package" lives. return folderName; } @@ -154,7 +191,7 @@ component * @package The package to get the default name for */ public function getDefaultName( required string package ){ - var packageParts = getPackageParts( package ); + var packageParts = getArtifactParts( package ); if ( packageParts.artifactId.len() ) { return packageParts.artifactId; @@ -168,6 +205,108 @@ component ); } + /** + * checks if an artifact exists in the given repository and gets it + * @repo The repository to check (URL or alias) + * @groupId The group ID of the artifact + * @artifactId The artifact ID + * @version The version of the artifact + */ + private struct function getArtifactFromRepo( + string repo, + string groupId, + string artifactId, + string version + ){ + switch ( repo ) { + case "https://maven-central.storage.googleapis.com/maven2/": + case "mavenCentral": + case "maven": + return getArtifactFromMavenCentral( groupId, artifactId, version ); + case "https://oss.sonatype.org/content/repositories/releases/": + case "https://jitpack.io/": + case "https://maven.google.com/": + case "https://repo.spring.io/release/": + case "https://repository.jboss.org/nexus/content/repositories/releases/": + case "https://repository.apache.org/snapshots/": + case "https://plugins.gradle.org/m2/": + throw "Repo not implemented yet: " & repo; + break; + default: + throw "Unsupported repository: " & repo; + } + } + + /** + * Get an artifact from Maven Central + * @groupId The group ID of the artifact + * @artifactId The artifact ID + * @version The version of the artifact + */ + private function getArtifactFromMavenCentral( + string groupId, + string artifactId, + string version + ){ + var artifact = { + "jarFileURL" : "", + "artifactMetadata" : {} + } + // get artifact metadata to make sure it exists + try { + var artifact.artifactMetadata = getArtifactMetadataFromMaven( arguments.groupId, arguments.artifactId ); + } catch ( Any e ) { + throw( + "Could not find artifact metadata for [#arguments.groupId#:#arguments.artifactId#] in maven central repository", + "endpointException", + e.detail + ); + } + + // Get latest version if not specified + if ( arguments.version eq "STABLE" ) { + latestVersion = getLatestVersion( arguments.groupId, arguments.artifactId ); + artifact.jarFileURL = getJarFileURL( + arguments.groupId, + arguments.artifactId, + latestVersion + ); + return artifact; + } else { + // Check if the version is a range + if ( !semanticVersion.isExactVersion( arguments.version ) ) { + if ( + artifact.artifactMetadata.keyExists( "versioning" ) && artifact.artifactMetadata.versioning.keyExists( "versions" ) && artifact.artifactMetadata.versioning.versions.len() + ) { + var sortedVersions = artifact.artifactMetadata.versioning.versions.sort( ( a, b ) => variables.semanticVersion.compare( b, a ) ); + // Get the latest version that matches the range + for ( var thisVersion in sortedVersions ) { + if ( semanticVersion.satisfies( thisVersion, arguments.version ) ) { + artifact.jarFileURL = getJarFileURL( + arguments.groupId, + arguments.artifactId, + thisVersion + ); + return artifact; + } + } + // If no version was found, throw an error + throw( "Could not find a version that satisfies the range: #arguments.version#" ); + } else { + throw( "Could not find versions in artifact metadata" ); + } + } + // Get artifact for the passed in version + artifact.jarFileURL = getJarFileURL( + arguments.groupId, + arguments.artifactId, + arguments.version + ); + + return artifact; + } + } + /** * Get an update for a package * @package The package name @@ -181,97 +320,78 @@ component required string version, boolean verbose = false ){ - // Review this logic and use semver for version comparison - packageVersion = guessVersionFromURL( package ); - // No version could be determined from package URL - if ( packageVersion == defaultVersion ) { - return { - isOutdated : true, - version : "unknown" - }; - // Our package URL has a version and it's the same as what's installed - } else if ( packageVersion == version ) { - return { - isOutdated : false, - version : version - }; - // our package URL has a version and it's not what's installed - } else { - return { isOutdated : true, version : version }; - } + return { + isOutdated : false, + version : "unknown" + }; } - // Helper function to get the latest version of an artifact - private function getLatestVersion( - string groupId, - string artifactId, - string repoURL = getRepositoryBaseURL() - ){ - var metadata = getArtifactMetadata( groupId, artifactId, repoURL ); + /** + * Get the latest version of an artifact + * @groupId The group ID of the artifact + * @artifactId The artifact ID + */ + private function getLatestVersion( string groupId, string artifactId ){ + var metadata = getArtifactMetadataFromMaven( groupId, artifactId ); - if ( metadata.keyExists( "versioning" ) && metadata.versioning.keyExists( "latest" ) ) { - return metadata.versioning.latest; + if ( metadata.keyExists( "versioning" ) && metadata.versioning.keyExists( "release" ) ) { + return metadata.versioning.release; } else { return "unknown"; } } - // Helper function to get the parts of a package string - private function getPackageParts( string package ){ + /** + * Get the parts of a Maven package string + * @package The package string + */ + private function getArtifactParts( string package ){ var response = { - "repoURL" : getRepositoryBaseURL(), + "repo" : "", "groupId" : "", "artifactId" : "", "version" : "" }; // Remove the 'maven:' prefix from the package - package = replace( package, "maven:", "", "one" ); + var packageId = replace( + arguments.package, + "maven:", + "", + "one" + ); // Split the package string by '|' to separate the repo and package - var parts = package.split( "|" ); + var parts = listToArray( packageId, "|" ); // Determine if a custom repo is provided - if ( arrayLen( parts ) == 2 ) { - response.repoURL = parts[ 1 ]; // Use custom repo URL - package = parts[ 2 ]; // The actual package + if ( arrayLen( parts ) > 1 ) { + response.repo = parts[ 1 ]; // Use custom repo + packageId = parts[ 2 ]; // The actual package } // Split the package into its components - var packageParts = package.split( ":" ); + var packageParts = listToArray( packageId, ":" ); - // Make sure we have at least a groupId and artifactId + // Make sure we have at least the groupId and artifactId if ( arrayLen( packageParts ) < 2 ) { - throw( "Invalid Maven package string: #package#" ); + throw( "Invalid Maven package string: #packageId#" ); } else { response.groupId = packageParts[ 1 ]; response.artifactId = packageParts[ 2 ]; - response.version = packageParts[ 3 ] ?: "LATEST"; // Default to LATEST if not provided + response.version = packageParts[ 3 ] ?: "STABLE"; // Default to STABLE if not provided } return response; } - // Helper function to get the parts of a package string - private function guessVersionFromURL( required string package ){ - var version = package; - if ( version contains "/" ) { - var version = version.reReplaceNoCase( "^([\w:]+)?//", "" ).listRest( "/\" ); - } - if ( version.refindNoCase( ".*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*" ) ) { - version = version.reReplaceNoCase( ".*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*", "\1" ); - } else { - version = defaultVersion; - } - return version; - } - - // Helper function to get the artifact metadata - private function getArtifactMetadata( - groupId, - artifactId, - repoURL = getRepositoryBaseURL() - ){ + /** + * Get the metadata for an artifact from Maven Central + * @groupId The group ID of the artifact + * @artifactId The artifact ID + */ + private function getArtifactMetadataFromMaven( groupId, artifactId ){ + var repoURL = getGlobalRepos().mavenCentral; var addr = repoURL & replace( groupId, ".", "/", "ALL" ) & "/" & artifactId & "/"; var httpResult = ""; var metaData = ""; @@ -304,7 +424,12 @@ component metaData = xmlParse( httpResult.fileContent ); md.groupId = metaData.xmlRoot.groupId.XmlText; md.artifactId = metaData.xmlRoot.artifactId.XmlText; - if ( structKeyExists( metaData.xmlRoot, "versioning" ) ) { + if ( + structKeyExists( metaData.xmlRoot, "versioning" ) && structKeyExists( + metaData.xmlRoot.versioning, + "latest" + ) && structKeyExists( metaData.xmlRoot.versioning, "release" ) + ) { md.versioning.latest = metaData.xmlRoot.versioning.latest.XmlText; md.versioning.release = metaData.xmlRoot.versioning.release.XmlText; for ( local.version in metaData.xmlRoot.versioning.versions.XmlChildren ) { @@ -320,14 +445,17 @@ component return md; } - // Helper function to get the artifact version + /** + * Get the version of an artifact from Maven Central + * @groupId The group ID of the artifact + * @artifactId The artifact ID + * @version The version of the artifact + */ private function getArtifactVersion( groupId, artifactId, version ){ - var addr = getRepositoryBaseURL() & replace( groupId, ".", "/", "ALL" ) & "/" & artifactId & "/" & version & "/" & artifactId & "-" & version & ".pom"; + var job = wirebox.getInstance( "interactiveJob" ); + var addr = getGlobalRepos().mavenCentral & replace( groupId, ".", "/", "ALL" ) & "/" & artifactId & "/" & version & "/" & artifactId & "-" & version & ".pom"; var httpResult = ""; - var job = wirebox.getInstance( "interactiveJob" ); - job.addLog( "Address: [#addr#]" ); - if ( configService.getSetting( "offlineMode", false ) ) { throw( "Can't download [#getNamePrefixes()#:#artifactId#], CommandBox is in offline mode. Go online with [config set offlineMode=false].", @@ -349,7 +477,14 @@ component } } - // Helper function to get the artifact and dependency jar URLs + /** + * Get the URLs for the artifact and its dependencies + * @groupId The group ID of the artifact + * @artifactId The artifact ID + * @version The version of the artifact + * @scopes The scopes to include + * @depth The depth of the dependencies + */ private function getArtifactAndDependencyJarURLs( groupId, artifactId, @@ -382,7 +517,7 @@ component continue; } if ( !cache.keyExists( dep.groupId & "/" & dep.artifactId ) ) { - d = getArtifactMetadata( dep.groupId, dep.artifactId ); + d = getArtifactMetadataFromMaven( dep.groupId, dep.artifactId ); if ( len( dep.version ) ) { d.wantedVersion = [ dep.version ]; } @@ -435,14 +570,22 @@ component return result; } - // Helper function to get the jar file URL + /** + * Get the URL for a JAR file + * @groupId The group ID of the artifact + * @artifactId The artifact ID + * @version The version of the artifact + */ private function getJarFileURL( groupId, artifactId, version ){ - var addr = getRepositoryBaseURL() & replace( groupId, ".", "/", "ALL" ) & "/" & artifactId & "/" & version & "/" & artifactId & "-" & version & ".jar"; + var addr = getGlobalRepos().mavenCentral & replace( groupId, ".", "/", "ALL" ) & "/" & artifactId & "/" & version & "/" & artifactId & "-" & version & ".jar"; return addr; } - // Helper function to parse the POM XML - public function parsePOM( xmlString ){ + /** + * Parse a POM file + * @xmlString The XML string to parse + */ + private function parsePOM( xmlString ){ var pom = { "name" : "", "packaging" : "", @@ -480,7 +623,11 @@ component return pom; } - // Helper function to parse the dependencies + /** + * Parse the dependencies from a POM file + * @rootXml The root XML object + * @node The node to parse + */ private function parseDependencies( rootXml, node ){ var dep = ""; var d = ""; @@ -530,7 +677,10 @@ component return deps; } - // Helper function to determine if XML is safe + /** + * Check if an XML string is safe + * @xml The XML string to check + */ private function isSafeXML( xml ){ if ( findNoCase( "!doctype", arguments.xml ) ) { return false; From 69fc132fc28c54a5bcfeed4b9e72b9cec1aee748 Mon Sep 17 00:00:00 2001 From: Javier Quintero Date: Mon, 24 Feb 2025 16:55:06 -0500 Subject: [PATCH 4/4] Bunch of changes to support multiple endpoints --- src/cfml/system/endpoints/Maven.cfc | 419 +++++++++++++-------- src/cfml/system/services/ConfigService.cfc | 2 + 2 files changed, 260 insertions(+), 161 deletions(-) diff --git a/src/cfml/system/endpoints/Maven.cfc b/src/cfml/system/endpoints/Maven.cfc index b753d8e99..dc97e97c0 100644 --- a/src/cfml/system/endpoints/Maven.cfc +++ b/src/cfml/system/endpoints/Maven.cfc @@ -23,22 +23,14 @@ component property name="wirebox" inject="wirebox"; // Properties - property name="namePrefixes" type="string"; - property name="globalRepos" type="struct"; + property name="namePrefixes" type="string"; + property name="defaultRepo" type="struct"; + property name="registeredRepos" type="struct"; // Constructor function init(){ setNamePrefixes( "maven" ); - var orderedStruct = structNew( "ordered" ); - orderedStruct[ "mavenCentral" ] = "https://maven-central.storage.googleapis.com/maven2/"; - orderedStruct[ "sonatype" ] = "https://oss.sonatype.org/content/repositories/releases/"; - orderedStruct[ "jitpack" ] = "https://jitpack.io/"; - orderedStruct[ "google" ] = "https://maven.google.com/"; - orderedStruct[ "spring" ] = "https://repo.spring.io/release/"; - orderedStruct[ "jboss" ] = "https://repository.jboss.org/nexus/content/repositories/releases/"; - orderedStruct[ "apacheSnapshots" ] = "https://repository.apache.org/snapshots/"; - orderedStruct[ "gradlePlugins" ] = "https://plugins.gradle.org/m2/"; - setGlobalRepos( orderedStruct ); + setDefaultRepo( { "mavenCentral" : "https://maven-central.storage.googleapis.com/maven2/" } ); return this; } @@ -54,27 +46,38 @@ component string currentWorkingDirectory = "", boolean verbose = false ){ - var job = wirebox.getInstance( "interactiveJob" ); - var repos = getGlobalRepos(); // Global linked struct of repos - // var projectRepos = {}; // TODO: Local overrides from box.json - // var repos = structAppend(globalRepos, projectRepos, false); // Preserve order - - var artifactParts = getArtifactParts( package ); - var jarFileURL = ""; - var artifact = { - "jarFileURL" : "", - "artifactMetadata" : {} + var job = wirebox.getInstance( "interactiveJob" ); + + variables.registeredRepos = variables.configService.getSetting( "endpoints.maven", getDefaultRepo() ); + // Preserve order and allow overrides + structAppend( + variables.registeredRepos, + getProjectRepos( currentWorkingDirectory ), + true + ); + + var artifact = { + "parts" : getArtifactParts( package ), + "jarFileURL" : "", + "metadata" : {} }; + // If the repo is empty, default it to mavenCentral + if ( !artifact.parts.repo.len() ) { + artifact.parts.repo = "mavenCentral"; + } + + job.addLog( "Resolving Maven artifact: #artifact.parts.repo#" ); + // If the local artifact exists, serve it if ( - artifactService.artifactExists( artifactParts.artifactId, artifactParts.version ) && artifactParts.version != "STABLE" && !semanticVersion.isExactVersion( - artifactParts.version, + artifactService.artifactExists( artifact.parts.repo & artifact.parts.groupId & artifact.parts.artifactId, artifact.parts.version ) && artifact.parts.version != "STABLE" && !semanticVersion.isExactVersion( + artifact.parts.version, true ) ) { job.addLog( "Lucky you, we found this version in local artifacts!" ); - var thisArtifactPath = artifactService.getArtifactPath( artifactParts.artifactId, artifactParts.version ); + var thisArtifactPath = artifactService.getArtifactPath( artifact.parts.repo & artifact.parts.groupId & artifact.parts.artifactId, artifact.parts.version ); // Return the path to the artifact return fileEndpoint.resolvePackage( @@ -85,27 +88,29 @@ component } // Check only the explicitly defined repo, if any - if ( len( artifactParts.repo ) ) { - artifact = getArtifactFromRepo( - artifactParts.repo, - artifactParts.groupId, - artifactParts.artifactId, - artifactParts.version + if ( len( artifact.parts.repo ) ) { + var returnedArtifact = getArtifactFromRepo( + artifact.parts.repo, + artifact.parts.groupId, + artifact.parts.artifactId, + artifact.parts.version ); - jarFileURL = artifact.jarFileURL; + artifact.metadata = returnedArtifact.metadata; + artifact.jarFileURL = returnedArtifact.jarFileURL; } // Otherwise, check each registered repo sequentially else { - for ( var alias in repos ) { - artifact = getArtifactFromRepo( - repos[ alias ], - artifactParts.groupId, - artifactParts.artifactId, - artifactParts.version + for ( var alias in getRegisteredRepos() ) { + var returnedArtifact = getArtifactFromRepo( + getRegisteredRepos()[ alias ], + artifact.parts.groupId, + artifact.parts.artifactId, + artifact.parts.version ); - jarFileURL = artifact.jarFileURL; + artifact.metadata = returnedArtifact.metadata; + artifact.jarFileURL = returnedArtifact.jarFileURL; // If we found the artifact, break out of the loop - if ( jarFileURL.len() ) { + if ( artifact.jarFileURL.len() ) { break; } } @@ -118,52 +123,63 @@ component arguments.verbose ); - if ( artifactParts.version eq "STABLE" ) { - artifactParts.version = getLatestVersion( artifactParts.groupId, artifactParts.artifactId ); + if ( artifact.parts.version eq "STABLE" ) { + artifact.parts.version = getLatestVersion( + artifact.parts.repo, + artifact.parts.groupId, + artifact.parts.artifactId + ); } // Update artifact version if it's a range - else if ( !semanticVersion.isExactVersion( artifactParts.version, true ) ) { + else if ( !semanticVersion.isExactVersion( artifact.parts.version, true ) ) { + job.addLog( "It's a range: #artifact.parts.version#" ); if ( - artifact.artifactMetadata.keyExists( "versioning" ) && artifact.artifactMetadata.versioning.keyExists( "versions" ) && artifact.artifactMetadata.versioning.versions.len() + artifact.metadata.keyExists( "versioning" ) && artifact.metadata.versioning.keyExists( "versions" ) && artifact.metadata.versioning.versions.len() ) { - var sortedVersions = artifact.artifactMetadata.versioning.versions.sort( ( a, b ) => variables.semanticVersion.compare( b, a ) ); + var sortedVersions = artifact.metadata.versioning.versions.sort( ( a, b ) => variables.semanticVersion.compare( b, a ) ); // Get the latest version that matches the range for ( var thisVersion in sortedVersions ) { - if ( semanticVersion.satisfies( thisVersion, artifactParts.version ) ) { - artifactParts.version = thisVersion; + if ( semanticVersion.satisfies( thisVersion, artifact.parts.version ) ) { + job.addLog( "VERSION FOUND: #thisVersion#" ); + artifact.parts.version = thisVersion; break; } } } } - // get dependencies + job.addLog( "VERSION: #artifact.parts.version#" ); + + // Get dependencies var artifactDependencies = getArtifactAndDependencyJarURLs( - artifactParts.groupId, - artifactParts.artifactId, - artifactParts.version + artifact.parts.repo, + artifact.parts.groupId, + artifact.parts.artifactId, + artifact.parts.version ); var installPaths = {}; var dependencies = {}; for ( var dependency in artifactDependencies ) { - if ( dependency.artifactId == artifactParts.artifactId ) { + if ( dependency.artifactId == artifact.parts.artifactId ) { continue; } - dependencies[ dependency.artifactId ] = getNamePrefixes() & ( - artifactParts.repo.len() ? artifactParts.repo & "|" : "" - ) & dependency.groupId & ":" & dependency.artifactId & ":" & dependency.version; + dependencies[ dependency.artifactId ] = getNamePrefixes() & ":" & ( + artifact.parts.repo.len() && artifact.parts.repo neq "mavenCentral" ? artifact.parts.repo & "|" : "" + ) & dependency.groupId & ":" & dependency.artifactId & ":" & convertMavenToNpmVersionRange( + dependency.version + ); installPaths[ dependency.artifactId ] = "lib/" & dependency.artifactId; } - // override the box.json with the actual version and dependencies + // Override the box.json with the actual version and dependencies var boxJSON = { - "name" : "#artifactParts.groupId & "-" & artifactParts.artifactId#.jar", - "slug" : artifactParts.artifactId, - "version" : artifactParts.version, - "location" : "maven:" & arguments.package, + "name" : "#artifact.parts.groupId & "-" & artifact.parts.artifactId#.jar", + "slug" : artifact.parts.groupId & "-" & artifact.parts.artifactId, + "version" : artifact.parts.version, + "location" : getNamePrefixes() & ":" & arguments.package, "type" : "jars", "dependencies" : dependencies, "installPaths" : installPaths @@ -173,10 +189,10 @@ component job.addLog( "Storing download in artifact cache..." ); - // store it locally in the artifact cache + // Store it locally in the artifact cache artifactService.createArtifact( - artifactParts.artifactId, - artifactParts.version, + artifact.parts.repo & artifact.parts.groupId & artifact.parts.artifactId, + artifact.parts.version, folderName ); @@ -206,58 +222,47 @@ component } /** - * checks if an artifact exists in the given repository and gets it - * @repo The repository to check (URL or alias) - * @groupId The group ID of the artifact - * @artifactId The artifact ID - * @version The version of the artifact + * Get the project repositories from the box.json file + * @currentWorkingDirectory The directory to get the repositories from */ - private struct function getArtifactFromRepo( - string repo, - string groupId, - string artifactId, - string version - ){ - switch ( repo ) { - case "https://maven-central.storage.googleapis.com/maven2/": - case "mavenCentral": - case "maven": - return getArtifactFromMavenCentral( groupId, artifactId, version ); - case "https://oss.sonatype.org/content/repositories/releases/": - case "https://jitpack.io/": - case "https://maven.google.com/": - case "https://repo.spring.io/release/": - case "https://repository.jboss.org/nexus/content/repositories/releases/": - case "https://repository.apache.org/snapshots/": - case "https://plugins.gradle.org/m2/": - throw "Repo not implemented yet: " & repo; - break; - default: - throw "Unsupported repository: " & repo; + function getProjectRepos( string currentWorkingDirectory ){ + var boxJSONPath = currentWorkingDirectory & "/box.json"; + + if ( fileExists( boxJSONPath ) ) { + var boxJSON = deserializeJSON( fileRead( boxJSONPath ) ); + + if ( structKeyExists( boxJSON, "mavenRepositories" ) ) { + return boxJSON.mavenRepositories; + } } + + return {}; } /** - * Get an artifact from Maven Central + * Checks if an artifact exists in the given repository and gets it + * @repo The repository to check (URL or alias) * @groupId The group ID of the artifact * @artifactId The artifact ID * @version The version of the artifact */ - private function getArtifactFromMavenCentral( + private struct function getArtifactFromRepo( + string repo, string groupId, string artifactId, string version ){ - var artifact = { - "jarFileURL" : "", - "artifactMetadata" : {} - } + var artifact = { "jarFileURL" : "", "metadata" : {} } // get artifact metadata to make sure it exists try { - var artifact.artifactMetadata = getArtifactMetadataFromMaven( arguments.groupId, arguments.artifactId ); + var artifact.metadata = getArtifactMetadataFromMaven( + arguments.repo, + arguments.groupId, + arguments.artifactId + ); } catch ( Any e ) { throw( - "Could not find artifact metadata for [#arguments.groupId#:#arguments.artifactId#] in maven central repository", + "Could not find artifact metadata for [#arguments.groupId#:#arguments.artifactId#] in #arguments.repo# repository", "endpointException", e.detail ); @@ -265,8 +270,13 @@ component // Get latest version if not specified if ( arguments.version eq "STABLE" ) { - latestVersion = getLatestVersion( arguments.groupId, arguments.artifactId ); + latestVersion = getLatestVersion( + arguments.repo, + arguments.groupId, + arguments.artifactId + ); artifact.jarFileURL = getJarFileURL( + arguments.repo, arguments.groupId, arguments.artifactId, latestVersion @@ -276,13 +286,14 @@ component // Check if the version is a range if ( !semanticVersion.isExactVersion( arguments.version ) ) { if ( - artifact.artifactMetadata.keyExists( "versioning" ) && artifact.artifactMetadata.versioning.keyExists( "versions" ) && artifact.artifactMetadata.versioning.versions.len() + artifact.metadata.keyExists( "versioning" ) && artifact.metadata.versioning.keyExists( "versions" ) && artifact.metadata.versioning.versions.len() ) { - var sortedVersions = artifact.artifactMetadata.versioning.versions.sort( ( a, b ) => variables.semanticVersion.compare( b, a ) ); + var sortedVersions = artifact.metadata.versioning.versions.sort( ( a, b ) => variables.semanticVersion.compare( b, a ) ); // Get the latest version that matches the range for ( var thisVersion in sortedVersions ) { if ( semanticVersion.satisfies( thisVersion, arguments.version ) ) { artifact.jarFileURL = getJarFileURL( + arguments.repo, arguments.groupId, arguments.artifactId, thisVersion @@ -298,6 +309,7 @@ component } // Get artifact for the passed in version artifact.jarFileURL = getJarFileURL( + arguments.repo, arguments.groupId, arguments.artifactId, arguments.version @@ -328,11 +340,20 @@ component /** * Get the latest version of an artifact + * @repo The repository to check (URL or alias) * @groupId The group ID of the artifact * @artifactId The artifact ID */ - private function getLatestVersion( string groupId, string artifactId ){ - var metadata = getArtifactMetadataFromMaven( groupId, artifactId ); + private function getLatestVersion( + string repo, + string groupId, + string artifactId + ){ + var metadata = getArtifactMetadataFromMaven( + arguments.repo, + arguments.groupId, + arguments.artifactId + ); if ( metadata.keyExists( "versioning" ) && metadata.versioning.keyExists( "release" ) ) { return metadata.versioning.release; @@ -387,11 +408,12 @@ component /** * Get the metadata for an artifact from Maven Central + * @repo The repository to check (URL or alias) * @groupId The group ID of the artifact * @artifactId The artifact ID */ - private function getArtifactMetadataFromMaven( groupId, artifactId ){ - var repoURL = getGlobalRepos().mavenCentral; + private function getArtifactMetadataFromMaven( repo, groupId, artifactId ){ + var repoURL = getRepoURL( arguments.repo ); var addr = repoURL & replace( groupId, ".", "/", "ALL" ) & "/" & artifactId & "/"; var httpResult = ""; var metaData = ""; @@ -445,15 +467,35 @@ component return md; } + /** + * Get the URL type of a repo + * @repo The repository to check (URL or alias) + */ + function getRepoURL( required string repo ){ + // Check if the repo is a known alias + if ( listFindNoCase( getRegisteredRepos().keyList(), arguments.repo ) ) { + return getRegisteredRepos()[ "#arguments.repo#" ]; + } + + // Check if it's a valid URL (starting with http:// or https://) + if ( reFindNoCase( "^(https?://)", arguments.repo ) ) { + return arguments.repo; + } + + // If it's neither an alias nor a valid URL, throw an error + throw "Invalid repository URL or alias: #arguments.repo#"; + } + /** * Get the version of an artifact from Maven Central + * @repo The repository URL to check * @groupId The group ID of the artifact * @artifactId The artifact ID * @version The version of the artifact */ - private function getArtifactVersion( groupId, artifactId, version ){ + private function getArtifactVersion( repo, groupId, artifactId, version ){ var job = wirebox.getInstance( "interactiveJob" ); - var addr = getGlobalRepos().mavenCentral & replace( groupId, ".", "/", "ALL" ) & "/" & artifactId & "/" & version & "/" & artifactId & "-" & version & ".pom"; + var addr = repo & replace( groupId, ".", "/", "ALL" ) & "/" & artifactId & "/" & version & "/" & artifactId & "-" & version & ".pom"; var httpResult = ""; if ( configService.getSetting( "offlineMode", false ) ) { @@ -479,6 +521,7 @@ component /** * Get the URLs for the artifact and its dependencies + * @repo The repository to check (URL or alias) * @groupId The group ID of the artifact * @artifactId The artifact ID * @version The version of the artifact @@ -486,101 +529,155 @@ component * @depth The depth of the dependencies */ private function getArtifactAndDependencyJarURLs( - groupId, - artifactId, - version, - scopes = "runtime,compile", - depth = 0 + repository, + groupIdentifier, + artifactIdentifier, + versionNumber, + scopes = "runtime,compile", + depthLevel = 0 ){ - var meta = getArtifactVersion( groupId, artifactId, version ); - var cache = {}; - var result = []; - var dep = ""; - var d = ""; - var v = ""; - if ( meta.packaging IS "jar" ) { - result = [ + var job = wirebox.getInstance( "interactiveJob" ); + var artifactMetadata = getArtifactVersion( + getRepoURL( arguments.repository ), + groupIdentifier, + artifactIdentifier, + versionNumber + ); + var dependencyCache = {}; + var jarDownloadList = []; + var dependency = ""; + var dependencyMetadata = ""; + var selectedVersion = ""; + + if ( artifactMetadata.packaging IS "jar" ) { + jarDownloadList = [ { - "download" : getJarFileURL( groupId, artifactId, version ), - "groupId" : arguments.groupId, - "artifactId" : arguments.artifactId, - "version" : arguments.version + "download" : getJarFileURL( + arguments.repository, + groupIdentifier, + artifactIdentifier, + versionNumber + ), + "groupId" : arguments.groupIdentifier, + "artifactId" : arguments.artifactIdentifier, + "version" : arguments.versionNumber } ]; } - for ( dep in meta.dependencies ) { - if ( !listFindNoCase( arguments.scopes, dep.scope ) ) { - // skip + + for ( dependency in artifactMetadata.dependencies ) { + if ( !listFindNoCase( arguments.scopes, dependency.scope ) ) { + // Skip dependencies that are not in the specified scopes continue; } - if ( dep.optional ) { + if ( dependency.optional ) { continue; } - if ( !cache.keyExists( dep.groupId & "/" & dep.artifactId ) ) { - d = getArtifactMetadataFromMaven( dep.groupId, dep.artifactId ); - if ( len( dep.version ) ) { - d.wantedVersion = [ dep.version ]; + if ( !dependencyCache.keyExists( dependency.groupId & "/" & dependency.artifactId ) ) { + dependencyMetadata = getArtifactMetadataFromMaven( + arguments.repository, + dependency.groupId, + dependency.artifactId + ); + if ( len( dependency.version ) ) { + dependencyMetadata.wantedVersion = [ dependency.version ]; } - cache[ dep.groupId & "/" & dep.artifactId ] = d; - } else if ( len( dep.version ) ) { - // add as a wanted version - arrayAppend( cache[ dep.groupId & "/" & dep.artifactId ].wantedVersion, dep.version ); + dependencyCache[ dependency.groupId & "/" & dependency.artifactId ] = dependencyMetadata; + } else if ( len( dependency.version ) ) { + // Add the specified version as a wanted version + arrayAppend( + dependencyCache[ dependency.groupId & "/" & dependency.artifactId ].wantedVersion, + dependency.version + ); } } - for ( dep in cache ) { - dep = cache[ dep ]; - if ( !dep.keyExists( "wantedVersion" ) ) { - v = dep.versioning.release; + for ( dependency in dependencyCache ) { + dependency = dependencyCache[ dependency ]; + if ( !dependency.keyExists( "wantedVersion" ) ) { + selectedVersion = dependency.versioning.release; } else { - // todo pick highest version - v = dep.wantedVersion[ 1 ]; + // TODO: Pick the highest version + selectedVersion = dependency.wantedVersion[ 1 ]; } - if ( dep.artifactId == arguments.artifactId && dep.groupId == arguments.groupId ) { + if ( dependency.artifactId == arguments.artifactIdentifier && dependency.groupId == arguments.groupIdentifier ) { continue; } - if ( meta.packaging IS "pom" && dep.scope IS "import" ) { - if ( depth > 10 ) { + if ( artifactMetadata.packaging IS "pom" && dependency.scope IS "import" ) { + if ( depthLevel > 10 ) { throw( message = "Maximum depth of 10 reached" ); } - d = getArtifactAndDependencyJarURLs( - dep.groupId, - dep.artifactId, - v, + dependencyMetadata = getArtifactAndDependencyJarURLs( + repository, + dependency.groupId, + dependency.artifactId, + selectedVersion, scopes, - depth++ + depthLevel++ ); - for ( v in d ) { - if ( !arrayFind( result, v ) ) { - arrayAppend( result, v ); + for ( selectedVersion in dependencyMetadata ) { + if ( !arrayFind( jarDownloadList, selectedVersion ) ) { + arrayAppend( jarDownloadList, selectedVersion ); } } } else { arrayAppend( - result, + jarDownloadList, { - "download" : getJarFileURL( dep.groupId, dep.artifactId, v ), - "groupId" : dep.groupId, - "artifactId" : dep.artifactId, - "version" : v + "download" : getJarFileURL( + repository, + dependency.groupId, + dependency.artifactId, + selectedVersion + ), + "groupId" : dependency.groupId, + "artifactId" : dependency.artifactId, + "version" : selectedVersion } ); } } - return result; + return jarDownloadList; } /** * Get the URL for a JAR file + * @repo The repository to check (URL or alias) * @groupId The group ID of the artifact * @artifactId The artifact ID * @version The version of the artifact */ - private function getJarFileURL( groupId, artifactId, version ){ - var addr = getGlobalRepos().mavenCentral & replace( groupId, ".", "/", "ALL" ) & "/" & artifactId & "/" & version & "/" & artifactId & "-" & version & ".jar"; + private function getJarFileURL( repo, groupId, artifactId, version ){ + var addr = getRepoURL( arguments.repo ) & replace( groupId, ".", "/", "ALL" ) & "/" & artifactId & "/" & version & "/" & artifactId & "-" & version & ".jar"; return addr; } + /** + * Converts a Maven-style version range to NPM-style semantic version constraints. + * @param range The Maven version range as a string (e.g., "[1.2.0,2.0.0)"). + * @return The equivalent NPM-style constraint (e.g., ">=1.2.0 <2.0.0"). + */ + function convertMavenToNpmVersionRange( required string range ){ + // If the range is an exact version, return it as-is + if ( semanticVersion.isExactVersion( range ) ) { + return range; + } + + var pattern = "([\[\(])([\d\.]+),([\d\.]+)([\]\)])"; + var matches = reFind( pattern, range, 1, true ); + + if ( !matches.len() ) { + throw( message = "Invalid version range format: #range#", type = "InvalidVersionRangeException" ); + } + + var lowerBoundSymbol = matches[ 2 ] EQ "[" ? ">=" : ">"; + var lowerVersion = matches[ 3 ]; + var upperVersion = matches[ 4 ]; + var upperBoundSymbol = matches[ 5 ] EQ "]" ? "<=" : "<"; + + return lowerBoundSymbol & lowerVersion & " " & upperBoundSymbol & upperVersion; + } + /** * Parse a POM file * @xmlString The XML string to parse diff --git a/src/cfml/system/services/ConfigService.cfc b/src/cfml/system/services/ConfigService.cfc index e02b1cff6..7656d9e8a 100644 --- a/src/cfml/system/services/ConfigService.cfc +++ b/src/cfml/system/services/ConfigService.cfc @@ -60,6 +60,8 @@ component accessors="true" singleton { 'endpoints.forgebox', 'endpoints.forgebox.APIToken', 'endpoints.forgebox.APIURL', + // Maven endpoints + 'endpoints.maven', // Servers 'server', 'server.singleServerMode',