blob: 0d91332820d178100c8efdf5a11c97a255159b84 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with this
* work for additional information regarding copyright ownership. The ASF
* licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.sling.feature.io.artifacts;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;
import org.apache.sling.feature.ArtifactId;
import org.apache.sling.feature.io.artifacts.spi.ArtifactProvider;
import org.apache.sling.feature.io.artifacts.spi.ArtifactProviderContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The artifact manager is the central service to get artifacts.
* It uses {@link ArtifactProvider}s to get artifacts. The
* providers are loaded using the service loader.
*/
public class ArtifactManager implements AutoCloseable {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/** The map of providers. */
private final Map<String, ArtifactProvider> providers;
/** The configuration */
private final ArtifactManagerConfig config;
/**
* Get an artifact manager based on the configuration
* @param config The configuration
* @return The artifact manager
* @throws IOException If the manager can't be initialized
*/
public static ArtifactManager getArtifactManager(final ArtifactManagerConfig config) throws IOException {
final ServiceLoader<ArtifactProvider> loader = ServiceLoader.load(ArtifactProvider.class);
final Map<String, ArtifactProvider> providers = new HashMap<>();
for(final ArtifactProvider provider : loader) {
providers.put(provider.getProtocol(), provider);
}
final String[] repositoryURLs = new String[config.getRepositoryUrls().length];
int index = 0;
for(final String urlString : config.getRepositoryUrls()) {
repositoryURLs[index] = urlString;
index++;
}
// default
if ( !providers.containsKey("*") ) {
providers.put("*", new DefaultArtifactHandler());
}
return new ArtifactManager(config, providers);
}
ArtifactManager(final ArtifactManagerConfig config, final Map<String, ArtifactProvider> providers)
throws IOException {
this.config = config;
this.providers = providers;
try {
for(final ArtifactProvider provider : this.providers.values()) {
provider.init(config);
}
} catch ( final IOException io) {
shutdown();
throw io;
}
}
/**
* Shutdown the artifact manager.
*/
public void shutdown() {
for(final ArtifactProvider provider : this.providers.values()) {
provider.shutdown();
}
this.providers.clear();
}
@Override
public void close()
{
shutdown();
}
private final URL getArtifactFromProviders(final String url, final String relativeCachePath) throws IOException {
final int pos = url.indexOf(":");
final String scheme = url.substring(0, pos);
ArtifactProvider provider = this.providers.get(scheme);
if ( provider == null ) {
provider = this.providers.get("*");
}
if ( provider == null ) {
throw new IOException("No URL provider found for " + url);
}
return provider.getArtifact(url, relativeCachePath);
}
/**
* Get the full artifact url and file for an artifact.
* @param url Artifact url or relative path.
* @return Absolute url and file in the form of a handler.
* @throws IOException If something goes wrong.
*/
public ArtifactHandler getArtifactHandler(final String url) throws IOException {
logger.debug("Trying to get artifact for {}", url);
final String path;
if ( url.startsWith("mvn:") ) {
// mvn url
path = ArtifactId.fromMvnUrl(url).toMvnPath();
} else if ( url.startsWith(":") ) {
// repository path
path = url.substring(1);
} else if ( url.indexOf(":/") > 0 ) {
// absolute URL
int pos = url.indexOf(":/") + 2;
while ( url.charAt(pos) == '/') {
pos++;
}
final URL file = this.getArtifactFromProviders(url, url.substring(pos));
if ( file == null ) {
throw new IOException("Artifact " + url + " not found.");
}
return new ArtifactHandler(url, file);
} else {
// file (either relative or absolute)
final File f = new File(url);
if ( !f.exists()) {
throw new IOException("Artifact " + url + " not found.");
}
return new ArtifactHandler(f.toURI().toString(), f.toURI().toURL());
}
logger.debug("Querying repositories for {}", path);
for(final String repoUrl : this.config.getRepositoryUrls()) {
final StringBuilder builder = new StringBuilder();
builder.append(repoUrl);
builder.append('/');
builder.append(path);
final String artifactUrl = builder.toString();
final int pos = artifactUrl.indexOf(":");
final String scheme = artifactUrl.substring(0, pos);
ArtifactProvider handler = this.providers.get(scheme);
if ( handler == null ) {
handler = this.providers.get("*");
}
if ( handler == null ) {
throw new IOException("No URL handler found for " + artifactUrl);
}
logger.debug("Checking {} to get artifact from {}", handler, artifactUrl);
final URL file = handler.getArtifact(artifactUrl, path);
if ( file != null ) {
logger.debug("Found artifact {}", artifactUrl);
return new ArtifactHandler(artifactUrl, file);
}
// check for SNAPSHOT
final int lastSlash = artifactUrl.lastIndexOf('/');
final int startSnapshot = artifactUrl.indexOf("-SNAPSHOT", lastSlash + 1);
if ( startSnapshot > -1 ) {
// special snapshot handling
final String metadataUrl = artifactUrl.substring(0, lastSlash) + "/maven-metadata.xml";
try {
final ArtifactHandler metadataHandler = this.getArtifactHandler(metadataUrl);
final String contents = getFileContents(metadataHandler);
final String latestVersion = getLatestSnapshot(contents);
if ( latestVersion != null ) {
final String name = artifactUrl.substring(lastSlash); // includes slash
final String fullURL = artifactUrl.substring(0, lastSlash) + name.replace("SNAPSHOT", latestVersion);
int pos2 = fullURL.indexOf(":/") + 2;
while ( fullURL.charAt(pos2) == '/') {
pos2++;
}
final URL file2 = this.getArtifactFromProviders(fullURL, path);
if ( file2 == null ) {
throw new IOException("Artifact " + fullURL + " not found.");
}
return new ArtifactHandler(artifactUrl, file2);
}
} catch ( final IOException ignore ) {
// we ignore this but report the original 404
}
}
}
throw new IOException("Artifact " + url + " not found in any repository.");
}
protected String getFileContents(final ArtifactHandler handler) throws IOException {
final StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(handler.getLocalURL().openStream(), "UTF-8"))) {
for(String line = reader.readLine(); line != null; line = reader.readLine()) {
sb.append(line).append('\n');
}
}
return sb.toString();
}
public static String getValue(final String xml, final String[] xpath) {
String value = null;
int pos = 0;
for(final String name : xpath) {
final String element = '<' + name + '>';
pos = xml.indexOf(element, pos);
if ( pos == -1 ) {
final String elementWithAttributes = '<' + name + ' ';
pos = xml.indexOf(elementWithAttributes, pos);
if ( pos == -1 ) {
break;
}
}
pos = xml.indexOf('>', pos) + 1;
}
if ( pos != -1 ) {
final int endPos = xml.indexOf("</", pos);
if ( endPos != -1 ) {
value = xml.substring(pos, endPos).trim();
}
}
return value;
}
public static String getLatestSnapshot(final String mavenMetadata) {
final String timestamp = getValue(mavenMetadata, new String[] {"metadata", "versioning", "snapshot", "timestamp"});
final String buildNumber = getValue(mavenMetadata, new String[] {"metadata", "versioning", "snapshot", "buildNumber"});
if ( timestamp != null && buildNumber != null ) {
return timestamp + '-' + buildNumber;
}
return null;
}
private static final class DefaultArtifactHandler implements ArtifactProvider {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private volatile File cacheDir;
private volatile ArtifactProviderContext config;
@Override
public String getProtocol() {
return "*";
}
@Override
public void init(final ArtifactProviderContext config) throws IOException {
this.cacheDir = config.getCacheDirectory();
this.config = config;
}
@Override
public void shutdown() {
this.config = null;
this.cacheDir = null;
}
@Override
public URL getArtifact(final String url, final String relativeCachePath) {
logger.debug("Checking url to be local file {}", url);
// check if this is already a local file
try {
final File f = new File(new URL(url).toURI());
if ( f.exists() ) {
this.config.incLocalArtifacts();
return f.toURI().toURL();
}
return null;
} catch ( final URISyntaxException ise) {
// ignore
} catch ( final IllegalArgumentException iae) {
// ignore
} catch ( final MalformedURLException mue) {
// ignore
}
logger.debug("Checking remote url {}", url);
try {
// check for url
if ( url.indexOf(":") == -1 ) {
return null;
}
final String filePath = (this.cacheDir.getAbsolutePath() + File.separatorChar + relativeCachePath).replace('/', File.separatorChar);
final File cacheFile = new File(filePath);
if ( !cacheFile.exists() ) {
cacheFile.getParentFile().mkdirs();
final URL u = new URL(url);
final URLConnection con = u.openConnection();
final String userInfo = u.getUserInfo();
if (userInfo != null) {
con.addRequestProperty("Authorization", "Basic " + Base64.getEncoder().encodeToString(u.toURI().getUserInfo().getBytes("UTF-8")));
}
con.connect();
final InputStream readIS = con.getInputStream();
final byte[] buffer = new byte[32768];
int l;
OutputStream os = null;
try {
os = new FileOutputStream(cacheFile);
while ( (l = readIS.read(buffer)) >= 0 ) {
os.write(buffer, 0, l);
}
} finally {
try {
readIS.close();
} catch ( final IOException ignore) {
// ignore
}
if ( os != null ) {
try {
os.close();
} catch ( final IOException ignore ) {
// ignore
}
}
}
this.config.incDownloadedArtifacts();
} else {
this.config.incCachedArtifacts();
}
return cacheFile.toURI().toURL();
} catch ( final FileNotFoundException e) {
logger.trace("File not found here (keep on looking): '{}'", url);
// Do not report if the file does not exist as we cycle through the various sources
return null;
} catch ( final Exception e) {
logger.info("Artifact not found in one repository", e);
// ignore for now
return null;
}
}
@Override
public String toString() {
return "DefaultArtifactHandler";
}
}
}