blob: d9f4dfe224eb7a02bec18b351b9c20b303c8c656 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2017 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Compeople AG (Stefan Liebig) - various ongoing maintenance
* Genuitec LLC - various bug fixes
* Sonatype, Inc. - transport split
* Yatta Solutions GmbH - bug 457619
*******************************************************************************/
package org.eclipse.equinox.internal.p2.artifact.repository;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.artifact.processors.pgp.PGPSignatureVerifier;
import org.eclipse.equinox.internal.p2.artifact.repository.simple.SimpleArtifactDescriptor;
import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
import org.eclipse.equinox.internal.p2.repository.Transport;
import org.eclipse.equinox.internal.provisional.p2.artifact.repository.processing.ProcessingStepHandler;
import org.eclipse.equinox.internal.provisional.p2.core.eventbus.IProvisioningEventBus;
import org.eclipse.equinox.internal.provisional.p2.repository.IStateful;
import org.eclipse.equinox.p2.core.ProvisionException;
import org.eclipse.equinox.p2.metadata.IArtifactKey;
import org.eclipse.equinox.p2.repository.artifact.IArtifactDescriptor;
import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository;
import org.eclipse.equinox.p2.repository.artifact.spi.ArtifactDescriptor;
import org.eclipse.osgi.util.NLS;
/**
* A request to mirror (copy) an artifact into a given destination artifact repository.
*/
public class MirrorRequest extends ArtifactRequest {
/**
* A Status code that represents an error while processing the artifact. This error is not
* related to transport, but rather a problem with the processing step.
*/
public static final int ARTIFACT_PROCESSING_ERROR = 2;
/**
* Maximum number of times a request for a single artifact should be tried
*/
private static final int MAX_RETRY_REQUEST = 200;
/**
* The name of a repository property on an artifact repository, indicating the base URI
* to be used for reporting download statistics.
*/
private static final String PROP_STATS_URI = "p2.statsURI"; //$NON-NLS-1$
/**
* The name of a property on an artifact descriptor, indicating the relative download URI
* to be used to report download statistics for that artifact. The value of this property,
* if present, is appended to the {@link #PROP_STATS_URI} to create the full URI
* for reporting download statistics for that artifact.
*/
private static final String PROP_DOWNLOAD_STATS = "download.stats"; //$NON-NLS-1$
/**
* The additional parameters for downloading statistics
*/
private String downloadStatsParamters;
protected final IArtifactRepository target;
private final Map<String, String> targetDescriptorProperties;
private final Map<String, String> targetRepositoryProperties;
protected IArtifactDescriptor descriptor;
public MirrorRequest(IArtifactKey key, IArtifactRepository targetRepository, Map<String, String> targetDescriptorProperties, Map<String, String> targetRepositoryProperties, Transport transport) {
this(key, targetRepository, targetDescriptorProperties, targetRepositoryProperties, transport, null);
}
public MirrorRequest(IArtifactKey key, IArtifactRepository targetRepository, Map<String, String> targetDescriptorProperties, Map<String, String> targetRepositoryProperties, Transport transport, String statsParameters) {
super(key, transport);
target = targetRepository;
if (targetDescriptorProperties == null || targetDescriptorProperties.isEmpty()) {
this.targetDescriptorProperties = null;
} else {
this.targetDescriptorProperties = new HashMap<>();
this.targetDescriptorProperties.putAll(targetDescriptorProperties);
}
if (targetRepositoryProperties == null || targetRepositoryProperties.isEmpty()) {
this.targetRepositoryProperties = null;
} else {
this.targetRepositoryProperties = new HashMap<>();
this.targetRepositoryProperties.putAll(targetRepositoryProperties);
}
this.downloadStatsParamters = statsParameters;
}
@Override
protected void setSourceRepository(IArtifactRepository value) {
if (value != getSourceRepository()) {
// This refreshes the descriptor from the new repository as it might be different.
// If a MirrorRequest fails for a local repository the descriptor might indicate a "folder based" artifact
// which causes the MirrorRequest to fail again with a remote repository if the descriptor is not fetched again.
// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=457619
descriptor = null;
}
super.setSourceRepository(value);
}
@Override
public void perform(IArtifactRepository sourceRepository, IProgressMonitor monitor) {
monitor.subTask(NLS.bind(Messages.downloading, getArtifactKey().getId()));
setSourceRepository(sourceRepository);
// Do we already have the artifact in the target?
if (target.contains(getArtifactKey())) {
setResult(new Status(IStatus.OK, Activator.ID, NLS.bind(Messages.available_already_in, getArtifactKey())));
return;
}
// if the request does not have a descriptor then try to fill one in by getting
// the list of all and randomly picking one that appears to be optimized.
IArtifactDescriptor optimized = null;
IArtifactDescriptor canonical = null;
if (descriptor == null) {
IArtifactDescriptor[] descriptors = source.getArtifactDescriptors(getArtifactKey());
if (descriptors.length > 0) {
for (IArtifactDescriptor descriptor2 : descriptors) {
if (descriptor2.getProperty(IArtifactDescriptor.FORMAT) == null)
canonical = descriptor2;
else if (ProcessingStepHandler.canProcess(descriptor2))
optimized = descriptor2;
}
boolean chooseCanonical = source.getLocation().getScheme().equals("file"); //$NON-NLS-1$
// If the source repo is local then look for a canonical descriptor so we don't waste processing time.
descriptor = chooseCanonical ? canonical : optimized;
// if the descriptor is still null then we could not find our first choice of format so switch the logic.
if (descriptor == null)
descriptor = !chooseCanonical ? canonical : optimized;
}
}
// if the descriptor is not set now then the repo does not have the requested artifact
// TODO improve the reporting here. It may be the case that the repo has the artifact
// but the client does not have a processor
if (descriptor == null) {
setResult(new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.artifact_not_found, getArtifactKey())));
return;
}
IArtifactDescriptor destinationDescriptor = getDestinationDescriptor(descriptor, descriptor == canonical);
IStatus status = transfer(destinationDescriptor, descriptor, monitor);
// if ok, cancelled or transfer has already been done with the canonical form return with status set
if (status.getSeverity() == IStatus.CANCEL) {
setResult(status);
return;
}
if (monitor.isCanceled()) {
setResult(Status.CANCEL_STATUS);
return;
}
if (status.isOK()) {
setResult(status);
return;
}
// failed, first remove possibly erroneously added descriptor
if (target.contains(destinationDescriptor))
target.removeDescriptor(destinationDescriptor);
if (descriptor == canonical || canonical == null) {
setResult(status);
return;
}
IStatus canonicalStatus = transfer(getDestinationDescriptor(canonical, true), canonical, monitor);
// To prevent the optimized transfer status severity from dominating the canonical, only merge
// if the canonical severity is equal to or higher than the optimized transfer severity.
if (canonicalStatus.getSeverity() < status.getSeverity())
setResult(canonicalStatus);
else
setResult(new MultiStatus(Activator.ID, canonicalStatus.getCode() != 0 ? canonicalStatus.getCode() : status.getCode(), new IStatus[] {status, canonicalStatus}, Messages.MirrorRequest_multipleDownloadProblems, null));
}
private IArtifactDescriptor getDestinationDescriptor(IArtifactDescriptor sourceDescriptor, boolean isCanonical) {
// Get the descriptor to use to store the artifact
// Since we are mirroring, ensure we clear out data from the original descriptor that may
// not apply in the new repo location.
// TODO this is brittle. perhaps the repo itself should do this? there are cases where
// we really do need to give the repo the actual descriptor to use however...
IArtifactDescriptor destinationDescriptor = target.createArtifactDescriptor(sourceDescriptor.getArtifactKey());
// destinationDescriptor.setProcessingSteps(EMPTY_STEPS);
// destinationDescriptor.setProperty(IArtifactDescriptor.DOWNLOAD_MD5, null);
// destinationDescriptor.setProperty(IArtifactDescriptor.DOWNLOAD_CONTENTTYPE, null);
// destinationDescriptor.setProperty(IArtifactDescriptor.FORMAT, null);
if (targetDescriptorProperties != null && destinationDescriptor instanceof ArtifactDescriptor)
((ArtifactDescriptor) destinationDescriptor).addProperties(targetDescriptorProperties);
if (targetRepositoryProperties != null && destinationDescriptor instanceof SimpleArtifactDescriptor)
((SimpleArtifactDescriptor) destinationDescriptor).addRepositoryProperties(targetRepositoryProperties);
if (isCanonical && destinationDescriptor instanceof ArtifactDescriptor) {
// keep some safety properties
if (sourceDescriptor.getProperty(PGPSignatureVerifier.PGP_SIGNATURES_PROPERTY_NAME) != null) {
((ArtifactDescriptor) destinationDescriptor)
.addProperties(Map.of(PGPSignatureVerifier.PGP_SIGNATURES_PROPERTY_NAME,
sourceDescriptor.getProperty(PGPSignatureVerifier.PGP_SIGNATURES_PROPERTY_NAME)));
}
}
return destinationDescriptor;
}
/**
* Keep retrying the source repository until it reports back that it will be impossible
* to get the artifact from it.
* @param destinationDescriptor
* @param sourceDescriptor
* @param monitor
* @return the status of the transfer operation
*/
protected IStatus transfer(IArtifactDescriptor destinationDescriptor, IArtifactDescriptor sourceDescriptor, IProgressMonitor monitor) {
MultiStatus allResults = new MultiStatus(Activator.ID, 0, NLS.bind(Messages.MirrorRequest_transferFailed, sourceDescriptor), null);
IStatus lastResult = Status.OK_STATUS;
// go until we get one (OK), there are no more mirrors to consider or the operation is cancelled.
// Put a hard limit of MAX_RETRY_REQUEST so we don't end up in an infinite loop.
// Really, if you've tried MAX_RETRY_REQUEST times without success, it's time to give up
// TODO Should we log that we gave up because of the retry count?
// TODO this needs to be redone with a much better mirror management scheme.
int counter = 0;
do {
lastResult = transferSingle(destinationDescriptor, sourceDescriptor, monitor);
allResults.add(lastResult);
if (lastResult.getException() instanceof Error) {
// Error is more severe than Exception - e.g. an OutOfMemoryError should not be ignored and might hinder further processing
throw (Error) lastResult.getException();
}
} while (lastResult.getSeverity() == IStatus.ERROR && lastResult.getCode() == IArtifactRepository.CODE_RETRY && counter++ < MAX_RETRY_REQUEST);
IProvisioningEventBus bus = source.getProvisioningAgent().getService(IProvisioningEventBus.class);
if (bus != null)
bus.publishEvent(new MirrorEvent(source, sourceDescriptor, lastResult.isOK() ? lastResult : (allResults.getChildren().length <= 1 ? lastResult : allResults)));
if (lastResult.isOK()) {
collectStats(sourceDescriptor, monitor);
return lastResult;
} else if (allResults.getChildren().length <= 1) {
return lastResult;
}
return allResults;
}
/**
* Collect download statistics, if specified by the descriptor and the source repository
*/
private void collectStats(IArtifactDescriptor sourceDescriptor, IProgressMonitor monitor) {
final String statsProperty = sourceDescriptor.getProperty(PROP_DOWNLOAD_STATS);
if (statsProperty == null)
return;
String statsRoot = sourceDescriptor.getRepository().getProperties().get(PROP_STATS_URI);
if (statsRoot == null)
return;
URI statsURI;
try {
statsURI = URIUtil.append(new URI(statsRoot), statsProperty);
if (downloadStatsParamters != null) {
try {
statsURI = new URI(statsURI.getScheme(), statsURI.getUserInfo(), statsURI.getHost(), statsURI.getPort(), statsURI.getPath(), statsURI.getQuery() == null ? downloadStatsParamters : downloadStatsParamters + "&" + statsURI.getQuery(), statsURI.getFragment()); //$NON-NLS-1$
} catch (URISyntaxException e) {
LogHelper.log(new Status(IStatus.WARNING, Activator.ID, "Unable to create download statistics due to invalid URL query: " + statsRoot + " suffix: " + statsProperty + " query: " + downloadStatsParamters)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}
} catch (URISyntaxException e) {
LogHelper.log(new Status(IStatus.WARNING, Activator.ID, "Unable to report download statistics due to invalid URL: " + statsRoot + " suffix: " + statsProperty)); //$NON-NLS-1$ //$NON-NLS-2$
return;
}
try {
transport.getLastModified(statsURI, monitor);
} catch (FileNotFoundException e) {
//ignore because it is expected that the statistics URI doesn't represent an existing file
} catch (Exception e) {
LogHelper.log(new Status(IStatus.WARNING, Activator.ID, "Failure reporting download statistics to URL: " + statsURI, e)); //$NON-NLS-1$
}
}
private IStatus transferSingle(IArtifactDescriptor destinationDescriptor, IArtifactDescriptor sourceDescriptor, IProgressMonitor monitor) {
OutputStream destination;
try {
destination = target.getOutputStream(destinationDescriptor);
} catch (ProvisionException e) {
return e.getStatus();
}
IStatus status = null;
Throwable priorException = null;
// Do the actual transfer
try {
status = getArtifact(sourceDescriptor, destination, monitor);
if (destination instanceof IStateful && status != null && !status.isOK()) {
IStatus destStatus = ((IStateful) destination).getStatus();
IStatus root = extractRootCause(status);
Throwable e = root != null ? root.getException() : null;
((IStateful) destination).setStatus(new MultiStatus(Activator.ID, status.getCode(), new IStatus[] {status, destStatus}, status.getMessage(), e));
}
} catch (RuntimeException e) {
priorException = e;
throw e;
} catch (Error e) {
priorException = e;
throw e;
} finally {
try {
destination.close();
} catch (IOException e) {
if (priorException == null) // don't mask a formerly thrown Exception/Error
{
if (status != null && status.getSeverity() == IStatus.ERROR && status.getCode() == IArtifactRepository.CODE_RETRY)
return new MultiStatus(Activator.ID, status.getCode(), new IStatus[] {status}, NLS.bind(Messages.error_closing_stream, getArtifactKey(), target.getLocation()), e);
return new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.error_closing_stream, getArtifactKey(), target.getLocation()), e);
}
// otherwise it is already thrown
}
}
return status;
}
protected IStatus getArtifact(IArtifactDescriptor sourceDescriptor, OutputStream destination, IProgressMonitor monitor) {
return getSourceRepository().getArtifact(sourceDescriptor, destination, monitor);
}
/**
* Extract the root cause. The root cause is the first severe non-MultiStatus status
* containing an exception when searching depth first otherwise null.
* @param status
* @return root cause
*/
private static IStatus extractRootCause(IStatus status) {
if (status == null)
return null;
if (!status.isMultiStatus())
return constraintStatus(status);
IStatus[] children = ((MultiStatus) status).getChildren();
if (children == null)
return constraintStatus(status);
for (IStatus element : children) {
IStatus deeper = extractRootCause(element);
if (deeper != null)
return deeper;
}
return constraintStatus(status);
}
private static IStatus constraintStatus(IStatus status) {
return status.getSeverity() == IStatus.ERROR && status.getException() != null ? status : null;
}
@Override
public String toString() {
return Messages.mirroring + getArtifactKey() + " into " + target; //$NON-NLS-1$
}
}