package io.takari.aether.localrepo;
/*******************************************************************************
* Copyright (c) 2010, 2011 Sonatype, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Sonatype, Inc. - initial API and implementation
*******************************************************************************/
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.SessionData;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.impl.UpdateCheck;
import org.eclipse.aether.impl.UpdateCheckManager;
import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
import org.eclipse.aether.metadata.Metadata;
import org.eclipse.aether.repository.AuthenticationDigest;
import org.eclipse.aether.repository.Proxy;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ResolutionErrorPolicy;
import org.eclipse.aether.resolution.ResolutionErrorPolicyRequest;
import org.eclipse.aether.transfer.ArtifactNotFoundException;
import org.eclipse.aether.transfer.ArtifactTransferException;
import org.eclipse.aether.transfer.MetadataNotFoundException;
import org.eclipse.aether.transfer.MetadataTransferException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Named("takari")
@Singleton
public class TakariUpdateCheckManager implements UpdateCheckManager {
private Logger logger = LoggerFactory.getLogger(TakariUpdateCheckManager.class);
private static final String ERROR_FLAG = "maven.retryOnDownloadError";
@Inject
private UpdatePolicyAnalyzer updatePolicyAnalyzer;
private static final String UPDATED_KEY_SUFFIX = ".lastUpdated";
private static final String ERROR_KEY_SUFFIX = ".error";
private static final String NOT_FOUND = "";
private static final String SESSION_CHECKS = "updateCheckManager.checks";
private final boolean allowImmediateRetryOfDownloadFailures;
public TakariUpdateCheckManager() {
if (System.getProperty(ERROR_FLAG) != null) {
this.allowImmediateRetryOfDownloadFailures = Boolean.getBoolean(ERROR_FLAG);
} else {
this.allowImmediateRetryOfDownloadFailures = false;
}
}
public UpdateCheckManager setUpdatePolicyAnalyzer(UpdatePolicyAnalyzer updatePolicyAnalyzer) {
this.updatePolicyAnalyzer = updatePolicyAnalyzer;
return this;
}
@Override
public void checkArtifact(RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check) {
if (check.getLocalLastUpdated() != 0 && !isUpdatedRequired(session, check.getLocalLastUpdated(), check.getPolicy())) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped remote update check for " + check.getItem() + ", locally installed artifact up-to-date.");
}
check.setRequired(false);
return;
}
Artifact artifact = check.getItem();
RemoteRepository repository = check.getRepository();
File artifactFile = check.getFile();
if (artifactFile == null) {
throw new IllegalArgumentException(String.format("The artifact '%s' has no file attached", artifact));
}
boolean fileExists = check.isFileValid() && artifactFile.exists();
File touchFile = getTouchFile(artifact, artifactFile);
Properties props = read(touchFile);
String updateKey = getUpdateKey(session, artifactFile, repository);
String dataKey = getDataKey(artifact, artifactFile, repository);
String error = getError(props, dataKey);
long lastUpdated;
if (fileExists) {
lastUpdated = artifactFile.lastModified();
} else if (error == null) {
// this is the first attempt ever
lastUpdated = 0;
} else if (error.length() <= 0) {
// artifact did not exist
lastUpdated = getLastUpdated(props, dataKey);
} else {
// artifact could not be transferred
String transferKey = getTransferKey(session, artifact, artifactFile, repository);
lastUpdated = getLastUpdated(props, transferKey);
}
if (isAlreadyUpdated(session.getData(), updateKey)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped remote update check for " + check.getItem() + ", already updated during this session.");
}
check.setRequired(false);
if (error != null) {
check.setException(newException(error, artifact, repository));
}
} else if (lastUpdated == 0) {
check.setRequired(true);
} else if (isUpdatedRequired(session, lastUpdated, check.getPolicy())) {
check.setRequired(true);
} else if (fileExists) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped remote update check for " + check.getItem() + ", locally cached artifact up-to-date.");
}
check.setRequired(false);
} else {
int errorPolicy = getPolicy(session, artifact, repository);
if (error == null || error.length() <= 0) {
if ((errorPolicy & ResolutionErrorPolicy.CACHE_NOT_FOUND) != 0) {
check.setRequired(false);
check.setException(newException(error, artifact, repository));
} else {
check.setRequired(true);
}
} else {
if ((errorPolicy & ResolutionErrorPolicy.CACHE_TRANSFER_ERROR) != 0) {
check.setRequired(false);
check.setException(newException(error, artifact, repository));
} else {
check.setRequired(true);
}
}
}
}
private ArtifactTransferException newException(String error, Artifact artifact, RemoteRepository repository) {
if (error == null || error.length() <= 0) {
return new ArtifactNotFoundException(artifact, repository, "Failure to find " + artifact + " in " + repository.getUrl() + " was cached in the local repository, "
+ "resolution will not be reattempted until the update interval of " + repository.getId() + " has elapsed or updates are forced");
} else {
return new ArtifactTransferException(artifact, repository, "Failure to transfer " + artifact + " from " + repository.getUrl() + " was cached in the local repository, "
+ "resolution will not be reattempted until the update interval of " + repository.getId() + " has elapsed or updates are forced. Original error: " + error);
}
}
@Override
public void checkMetadata(RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check) {
if (check.getLocalLastUpdated() != 0 && !isUpdatedRequired(session, check.getLocalLastUpdated(), check.getPolicy())) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped remote update check for " + check.getItem() + ", locally installed metadata up-to-date.");
}
check.setRequired(false);
return;
}
Metadata metadata = check.getItem();
RemoteRepository repository = check.getRepository();
File metadataFile = check.getFile();
if (metadataFile == null) {
throw new IllegalArgumentException(String.format("The metadata '%s' has no file attached", metadata));
}
boolean fileExists = check.isFileValid() && metadataFile.exists();
File touchFile = getTouchFile(metadata, metadataFile);
Properties props = read(touchFile);
String updateKey = getUpdateKey(session, metadataFile, repository);
String dataKey = getDataKey(metadata, metadataFile, check.getAuthoritativeRepository());
String error = getError(props, dataKey);
long lastUpdated;
if (error == null) {
if (fileExists) {
// last update was successful
lastUpdated = getLastUpdated(props, dataKey);
} else {
// this is the first attempt ever
lastUpdated = 0;
}
} else if (error.length() <= 0) {
// metadata did not exist
lastUpdated = getLastUpdated(props, dataKey);
} else {
// metadata could not be transferred
String transferKey = getTransferKey(session, metadata, metadataFile, repository);
lastUpdated = getLastUpdated(props, transferKey);
}
if (isAlreadyUpdated(session.getData(), updateKey)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped remote update check for " + check.getItem() + ", already updated during this session.");
}
check.setRequired(false);
if (error != null) {
check.setException(newException(error, metadata, repository));
}
} else if (lastUpdated == 0) {
check.setRequired(true);
} else if (isUpdatedRequired(session, lastUpdated, check.getPolicy())) {
check.setRequired(true);
} else if (fileExists) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped remote update check for " + check.getItem() + ", locally cached metadata up-to-date.");
}
check.setRequired(false);
} else {
int errorPolicy = getPolicy(session, metadata, repository);
if (error == null || error.length() <= 0) {
if ((errorPolicy & ResolutionErrorPolicy.CACHE_NOT_FOUND) != 0) {
check.setRequired(false);
check.setException(newException(error, metadata, repository));
} else {
check.setRequired(true);
}
} else {
if ((errorPolicy & ResolutionErrorPolicy.CACHE_TRANSFER_ERROR) != 0) {
check.setRequired(false);
check.setException(newException(error, metadata, repository));
} else {
check.setRequired(true);
}
}
}
}
private MetadataTransferException newException(String error, Metadata metadata, RemoteRepository repository) {
if (error == null || error.length() <= 0) {
return new MetadataNotFoundException(metadata, repository, "Failure to find " + metadata + " in " + repository.getUrl() + " was cached in the local repository, "
+ "resolution will not be reattempted until the update interval of " + repository.getId() + " has elapsed or updates are forced");
} else {
return new MetadataTransferException(metadata, repository, "Failure to transfer " + metadata + " from " + repository.getUrl() + " was cached in the local repository, "
+ "resolution will not be reattempted until the update interval of " + repository.getId() + " has elapsed or updates are forced. Original error: " + error);
}
}
private long getLastUpdated(Properties props, String key) {
String value = props.getProperty(key + UPDATED_KEY_SUFFIX, "");
try {
return (value.length() > 0) ? Long.parseLong(value) : 1;
} catch (NumberFormatException e) {
logger.debug("Cannot parse lastUpdated date: \'" + value + "\'. Ignoring.", e);
return 1;
}
}
private String getError(Properties props, String key) {
return props.getProperty(key + ERROR_KEY_SUFFIX);
}
private File getTouchFile(Artifact artifact, File artifactFile) {
return new File(artifactFile.getPath() + ".lastUpdated");
}
private File getTouchFile(Metadata metadata, File metadataFile) {
return new File(metadataFile.getParent(), "resolver-status.properties");
}
private String getDataKey(Artifact artifact, File artifactFile, RemoteRepository repository) {
Set<String> mirroredUrls = Collections.emptySet();
if (repository.isRepositoryManager()) {
mirroredUrls = new TreeSet<String>();
for (RemoteRepository mirroredRepository : repository.getMirroredRepositories()) {
mirroredUrls.add(normalizeRepoUrl(mirroredRepository.getUrl()));
}
}
StringBuilder buffer = new StringBuilder(1024);
buffer.append(normalizeRepoUrl(repository.getUrl()));
for (String mirroredUrl : mirroredUrls) {
buffer.append('+').append(mirroredUrl);
}
return buffer.toString();
}
private String getTransferKey(RepositorySystemSession session, Artifact artifact, File artifactFile, RemoteRepository repository) {
return getRepoKey(session, repository);
}
private String getDataKey(Metadata metadata, File metadataFile, RemoteRepository repository) {
return metadataFile.getName();
}
private String getTransferKey(RepositorySystemSession session, Metadata metadata, File metadataFile, RemoteRepository repository) {
return metadataFile.getName() + '/' + getRepoKey(session, repository);
}
private String getRepoKey(RepositorySystemSession session, RemoteRepository repository) {
StringBuilder buffer = new StringBuilder(128);
Proxy proxy = repository.getProxy();
if (proxy != null) {
buffer.append(AuthenticationDigest.forProxy(session, repository)).append('@');
buffer.append(proxy.getHost()).append(':').append(proxy.getPort()).append('>');
}
buffer.append(AuthenticationDigest.forRepository(session, repository)).append('@');
buffer.append(repository.getContentType()).append('-');
buffer.append(normalizeRepoUrl(repository.getUrl()));
return buffer.toString();
}
private String normalizeRepoUrl(String url) {
String result = url;
if (url != null && !url.endsWith("/")) {
result = url + '/';
}
return result;
}
private String getUpdateKey(RepositorySystemSession session, File file, RemoteRepository repository) {
return file.getAbsolutePath() + '|' + getRepoKey(session, repository);
}
private boolean isAlreadyUpdated(SessionData data, Object updateKey) {
Object checkedFiles = data.get(SESSION_CHECKS);
if (!(checkedFiles instanceof Map)) {
return false;
}
return ((Map<?, ?>) checkedFiles).containsKey(updateKey);
}
@SuppressWarnings("unchecked")
private void setUpdated(SessionData data, Object updateKey) {
Object checkedFiles = data.get(SESSION_CHECKS);
while (!(checkedFiles instanceof Map)) {
Object old = checkedFiles;
checkedFiles = new ConcurrentHashMap<Object, Object>(256);
if (data.set(SESSION_CHECKS, old, checkedFiles)) {
break;
}
checkedFiles = data.get(SESSION_CHECKS);
}
((Map<Object, Boolean>) checkedFiles).put(updateKey, Boolean.TRUE);
}
private boolean isUpdatedRequired(RepositorySystemSession session, long lastModified, String policy) {
return updatePolicyAnalyzer.isUpdatedRequired(session, lastModified, policy);
}
private Properties read(File touchFile) {
Properties props = new TrackingFileManager().read(touchFile);
return (props != null) ? props : new Properties();
}
@Override
public void touchArtifact(RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check) {
Artifact artifact = check.getItem();
File artifactFile = check.getFile();
File touchFile = getTouchFile(artifact, artifactFile);
String updateKey = getUpdateKey(session, artifactFile, check.getRepository());
String dataKey = getDataKey(artifact, artifactFile, check.getAuthoritativeRepository());
String transferKey = getTransferKey(session, artifact, artifactFile, check.getRepository());
setUpdated(session.getData(), updateKey);
Properties props = write(touchFile, dataKey, transferKey, check.getException());
if (artifactFile.exists() && !hasErrors(props)) {
touchFile.delete();
}
}
private boolean hasErrors(Properties props) {
for (Object key : props.keySet()) {
if (key.toString().endsWith(ERROR_KEY_SUFFIX)) {
return true;
}
}
return false;
}
@Override
public void touchMetadata(RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check) {
Metadata metadata = check.getItem();
File metadataFile = check.getFile();
File touchFile = getTouchFile(metadata, metadataFile);
String updateKey = getUpdateKey(session, metadataFile, check.getRepository());
String dataKey = getDataKey(metadata, metadataFile, check.getAuthoritativeRepository());
String transferKey = getTransferKey(session, metadata, metadataFile, check.getRepository());
setUpdated(session.getData(), updateKey);
write(touchFile, dataKey, transferKey, check.getException());
}
private Properties write(File touchFile, String dataKey, String transferKey, Exception error) {
Map<String, String> updates = new HashMap<String, String>();
String timestamp = Long.toString(System.currentTimeMillis());
if (error == null) {
updates.put(dataKey + ERROR_KEY_SUFFIX, null);
updates.put(dataKey + UPDATED_KEY_SUFFIX, timestamp);
updates.put(transferKey + UPDATED_KEY_SUFFIX, null);
} else if (error instanceof ArtifactNotFoundException || error instanceof MetadataNotFoundException) {
updates.put(dataKey + ERROR_KEY_SUFFIX, NOT_FOUND);
updates.put(dataKey + UPDATED_KEY_SUFFIX, timestamp);
updates.put(transferKey + UPDATED_KEY_SUFFIX, null);
} else {
String msg = error.getMessage();
if (msg == null || msg.length() <= 0) {
msg = error.getClass().getSimpleName();
}
updates.put(dataKey + ERROR_KEY_SUFFIX, msg);
updates.put(dataKey + UPDATED_KEY_SUFFIX, null);
updates.put(transferKey + UPDATED_KEY_SUFFIX, timestamp);
}
if (allowImmediateRetryOfDownloadFailures) {
// Don't track the error so that the next time Maven fires up it will retry to download
return new Properties();
} else {
return new TrackingFileManager().update(touchFile, updates);
}
}
private static int getPolicy(RepositorySystemSession session, Artifact artifact, RemoteRepository repository) {
ResolutionErrorPolicy rep = session.getResolutionErrorPolicy();
if (rep == null) {
return ResolutionErrorPolicy.CACHE_DISABLED;
}
return rep.getArtifactPolicy(session, new ResolutionErrorPolicyRequest<Artifact>(artifact, repository));
}
private static int getPolicy(RepositorySystemSession session, Metadata metadata, RemoteRepository repository) {
ResolutionErrorPolicy rep = session.getResolutionErrorPolicy();
if (rep == null) {
return ResolutionErrorPolicy.CACHE_DISABLED;
}
return rep.getMetadataPolicy(session, new ResolutionErrorPolicyRequest<Metadata>(metadata, repository));
}
}