jruby-gradle-plugin/core-plugin/src/main/groovy/com/github/jrubygradle/internal/core/IvyXmlRatpackProxyServer.gr...

171 lines
6.6 KiB
Groovy

package com.github.jrubygradle.internal.core
import com.github.jrubygradle.api.gems.GemInfo
import com.github.jrubygradle.api.gems.GemVersion
import com.github.jrubygradle.internal.gems.GemToIvy
import groovy.transform.CompileStatic
import groovy.transform.Synchronized
import groovy.util.logging.Slf4j
import org.ysb33r.grolifant.api.ExclusiveFileAccess
import ratpack.handling.RequestLogger
import ratpack.server.RatpackServer
import ratpack.server.ServerConfig
import java.nio.file.Files
import java.nio.file.Path
import static IvyUtils.revisionsAsHtmlDirectoryListing
import static com.github.jrubygradle.api.gems.GemVersion.gemVersionFromGradleIvyRequirement
import static java.nio.file.Files.move
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING
/** Uses Ratpack to run a small proxy server inside Gradle to proxy Rubygems.org
* as if it is local Ivy server with remote artifacts.
*
* @since 2.0
*/
@CompileStatic
@Slf4j
class IvyXmlRatpackProxyServer implements com.github.jrubygradle.api.core.IvyXmlProxyServer {
/** Implementation of a proxy server based upon Ratpack.
*
* @param cache Root directory for local Ivy XML cache.
* @param serverUri URI of remote Rubygems proxy.
* @param group Group that will be associated with the Rubygems proxy.
*/
IvyXmlRatpackProxyServer(File cache, URI serverUri, String group) {
localCachePath = cache
gemToIvy = new GemToIvy(serverUri)
api = new DefaultRubyGemRestApi(serverUri)
this.group = group
}
/** Tell the server to refresh dependencies upon a next run.
*
* @param refresh {@code true} to reload dependencies.
*/
@Override
void setRefreshDependencies(boolean refresh) {
refreshDependencies = refresh ? 1 : 0
}
/** Get the address of the local proxy.
*
* @return Local address as a URI.
*/
@Override
URI getBindAddress() {
"http://localhost:${server.bindPort}".toURI()
}
/** Start the proxy.
*
*/
@Override
@SuppressWarnings(['DuplicateStringLiteral', 'AbcMetric'])
void run() {
server = RatpackServer.start {
it.serverConfig(
ServerConfig.embedded()
.publicAddress('http://localhost'.toURI())
.port(0)
.baseDir(localCachePath)
).handlers { chain ->
chain.all(RequestLogger.ncsa())
chain.get("${this.group}/:module/:revision/ivy.xml") { ctx ->
String name = ctx.allPathTokens['module']
String revision = getGemQueryRevisionFromIvy(name, ctx.allPathTokens['revision'])
Path ivyXml = ivyFile(group, name, revision)
debug "Requested ${group}:${name}:${ctx.allPathTokens['revision']} translated to GEM with version ${revision}"
if (Files.notExists(ivyXml) || refreshDependencies) {
try {
createIvyXml(ivyXml, name, revision)
ctx.response.contentType('text/xml').sendFile(ivyXml)
} catch (com.github.jrubygradle.api.core.ApiException e) {
debug(e.message, e)
ctx.clientError(404)
}
} else {
ctx.response.contentType('text/xml').sendFile(ivyXml)
}
debug "Cached file is ${ivyXml.toAbsolutePath()}"
debug "Cached file contains ${ivyXml.text}"
}.get("${this.group}/:module/:revision/ivy.xml.sha1") { ctx ->
String name = ctx.allPathTokens['module']
String revision = getGemQueryRevisionFromIvy(name, ctx.allPathTokens['revision'])
Path ivyXml = ivyFile(group, name, revision)
Path ivyXmlSha1 = ivyXml.resolveSibling("${ivyXml.toFile().name}.sha1")
if (Files.exists(ivyXmlSha1)) {
ctx.response.contentType('text/plain').sendFile(ivyXmlSha1)
} else {
// TODO: Resolve ivy.xml first.
ctx.clientError(404)
}
}.get(':group/:module') { ctx ->
String grp = ctx.allPathTokens['group']
String name = ctx.allPathTokens['module']
debug "Request to find all versions for ${grp}:${name}"
List<String> versions = api.allVersions(name)
debug "Got versions ${versions.join(', ')}"
ctx.response.contentType('text/html').send(revisionsAsHtmlDirectoryListing(versions))
}.get { ctx ->
ctx.clientError(403)
}
}
}
debug "Ivy.xml proxy server starting on ${bindAddress}"
server
}
/** Returns the cache location for a specific GEM.
*
* @param group Group associated with GEM.
* @param name GEM name.
* @param revision GEM revision.
* @return Location of {@code ivy.xml} file.
*/
@SuppressWarnings('UnusedMethodParameter')
Path ivyFile(String group, String name, String revision) {
new File(localCachePath, "${name}/${revision}/ivy.xml").toPath()
}
@Synchronized
@SuppressWarnings('BuilderMethodWithSideEffects')
private void createIvyXml(Path ivyXml, String name, String revision) {
ExclusiveFileAccess efa = new ExclusiveFileAccess(120000, 20)
efa.access(ivyXml.toFile()) {
GemInfo gemInfo = api.metadata(name, revision)
ivyXml.parent.toFile().mkdirs()
Path tmp = ivyXml.resolveSibling("${ivyXml.toFile().name}.tmp")
tmp.withWriter { writer ->
gemToIvy.writeTo(writer, gemInfo)
}
move(tmp, ivyXml, ATOMIC_MOVE, REPLACE_EXISTING)
gemToIvy.writeSha1(ivyXml.toFile())
}
}
private String getGemQueryRevisionFromIvy(String gemName, String revisionPattern) {
GemVersion version = gemVersionFromGradleIvyRequirement(revisionPattern)
version.highOpenEnded ? api.latestVersion(gemName) : version.high
}
private void debug(String text) {
log.debug(text)
}
private void debug(String text, Object context) {
log.debug(text, context)
}
private volatile int refreshDependencies = 0
private RatpackServer server
private final File localCachePath
private final GemToIvy gemToIvy
private final com.github.jrubygradle.api.core.RubyGemQueryRestApi api
private final String group
}