jruby-gradle-plugin/core-plugin/src/main/groovy/com/github/jrubygradle/internal/core/AbstractIvyXmlProxyServer.g...

216 lines
7.7 KiB
Groovy

/*
* Copyright (c) 2014-2023, R. Tyler Croy <rtyler@brokenco.de>,
* Schalk Cronje <ysb33r@gmail.com>, Christian Meier, Lookout, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.github.jrubygradle.internal.core
import com.github.jrubygradle.api.core.ApiException
import com.github.jrubygradle.api.core.GemRepositoryConfiguration
import com.github.jrubygradle.api.core.IvyXmlProxyServer
import com.github.jrubygradle.api.core.RubyGemQueryRestApi
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.InheritConstructors
import groovy.transform.Synchronized
import groovy.util.logging.Slf4j
import org.ysb33r.grolifant.api.core.ExclusiveFileAccess
import java.nio.file.Files
import java.nio.file.Path
import java.time.Instant
import static com.github.jrubygradle.api.gems.GemVersion.gemVersionFromGradleIvyRequirement
import static com.github.jrubygradle.internal.core.IvyUtils.revisionsAsHtmlDirectoryListing
import static java.nio.file.Files.move
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING
/** Base class for implementing a proxy IvyXML server.
*
* @author Schalk W. Cronjé
*
* @since 2.0
*/
@CompileStatic
@Slf4j
abstract class AbstractIvyXmlProxyServer implements IvyXmlProxyServer {
@InheritConstructors
static class NotFound extends Exception {
}
/** 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:${bindPort}".toURI()
}
/** 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()
}
/** Implementation of a proxy server.
*
* @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.
* @param grc Additional configuration regarding remote GEM server
*/
protected AbstractIvyXmlProxyServer(
File cache,
URI serverUri,
String group,
GemRepositoryConfiguration grc
) {
localCachePath = cache
gemToIvy = new GemToIvy(serverUri)
api = new DefaultRubyGemRestApi(serverUri)
this.group = group
this.configuration = grc
}
@Synchronized
@SuppressWarnings('BuilderMethodWithSideEffects')
protected 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())
}
}
protected File getLocalCachePath() {
this.localCachePath
}
protected String getGroup() {
this.group
}
private boolean inGroups(String grp) {
grp == this.group
}
protected boolean expired(Path ivyXml) {
System.currentTimeMillis()
Path ivyXmlSha1 = ivyXml.resolveSibling("${ivyXml.toFile().name}.sha1")
Files.notExists(ivyXml) || Files.notExists(ivyXmlSha1) ||
(Files.getLastModifiedTime(ivyXml).toMillis() + EXPIRY_PERIOD_MILLIS < Instant.now().toEpochMilli())
}
protected Path getIvyXml(String grp, String name, String version) throws NotFound {
if (inGroups(grp)) {
String revision = getGemQueryRevisionFromIvy(name, version)
Path ivyXml = ivyFile(grp, name, revision)
debug "Requested ${group}:${name}:${version} translated to GEM with version ${revision}"
if (refreshDependencies || expired(ivyXml)) {
try {
createIvyXml(ivyXml, name, revision)
} catch (ApiException e) {
debug(e.message, e)
throw new NotFound()
}
}
debug "Cached file is ${ivyXml.toAbsolutePath()}"
debug "Cached file contains ${ivyXml.text}"
ivyXml
} else {
throw new NotFound()
}
}
protected Path getIvyXmlSha1(String grp, String name, String version) throws NotFound {
if (inGroups(grp)) {
Path ivyXml = getIvyXml(grp, name, version)
ivyXml.resolveSibling("${ivyXml.toFile().name}.sha1")
} else {
throw new NotFound()
}
}
protected String getDirectoryListing(String grp, String name) throws NotFound {
if (inGroups(grp)) {
debug "Request to find all versions for ${grp}:${name}"
List<String> versions = api.allVersions(name, configuration.prerelease)
debug "Got versions ${versions.join(', ')}"
revisionsAsHtmlDirectoryListing(versions)
} else {
throw new NotFound()
}
}
/** Get port the proxy server has bound to.
*
* @return Bind port
*/
abstract protected int getBindPort()
private String getGemQueryRevisionFromIvy(String gemName, String revisionPattern) {
GemVersion version = gemVersionFromGradleIvyRequirement(revisionPattern)
version.highOpenEnded ? api.latestVersion(gemName, configuration.prerelease) : version.high
}
private void debug(String text) {
log.debug(text)
}
private void debug(String text, Object context) {
log.debug(text, context)
}
private static final long EXPIRY_PERIOD_MILLIS =
System.getProperty('com.github.jrubygradle.cache-expiry-days', '15').toInteger() * 24 * 3600 * 1000
private volatile int refreshDependencies = 0
private final File localCachePath
private final GemToIvy gemToIvy
private final RubyGemQueryRestApi api
private final String group
private final GemRepositoryConfiguration configuration
}