[581519] Support adding PGP signatures to the bundle pool artifacts.xml

https://bugs.eclipse.org/bugs/show_bug.cgi?id=581519
diff --git a/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/CachingRepositoryManager.java b/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/CachingRepositoryManager.java
index fb7e59a..deadc96 100644
--- a/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/CachingRepositoryManager.java
+++ b/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/CachingRepositoryManager.java
@@ -204,7 +204,9 @@
         String[] allSuffixes = getAllSuffixes();
         String[] suffixes = sortSuffixes(allSuffixes, preferredOrder);
 
-        sub = SubMonitor.convert(sub, NLS.bind(Messages.CachingRepositoryManager_AddingRepository_task, location), suffixes.length * 100);
+        sub = SubMonitor.convert(sub, NLS.bind(Messages.CachingRepositoryManager_AddingRepository_task,
+            repositoryType == IRepository.TYPE_METADATA ? Messages.CachingRepositoryManager_metadata : Messages.CachingRepositoryManager_artifact, location),
+            suffixes.length * 100);
         ProvisionException failure = null;
 
         try
diff --git a/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/Messages.java b/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/Messages.java
index 662c872..a8f26e8 100644
--- a/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/Messages.java
+++ b/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/Messages.java
@@ -61,10 +61,14 @@
 
   public static String CachingRepositoryManager_AddingRepository_task;
 
+  public static String CachingRepositoryManager_artifact;
+
   public static String CachingRepositoryManager_Failure_message;
 
   public static String CachingRepositoryManager_LoadingFailed_message;
 
+  public static String CachingRepositoryManager_metadata;
+
   public static String CachingRepositoryManager_NonRelative_message;
 
   public static String CachingRepositoryManager_RepeatedDownload_task;
@@ -109,6 +113,8 @@
 
   public static String ProfileReferencerImpl_NotFile_exception;
 
+  public static String ProfileTransactionImpl_AddPGPSignature;
+
   public static String ProfileTransactionImpl_CollectingArtifacts_task;
 
   public static String ProfileTransactionImpl_CollectingArtifactsFor_task;
diff --git a/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/ProfileTransactionImpl.java b/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/ProfileTransactionImpl.java
index a3b473a..71527cb 100644
--- a/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/ProfileTransactionImpl.java
+++ b/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/ProfileTransactionImpl.java
@@ -39,6 +39,7 @@
 
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.FileLocator;
+import org.eclipse.core.runtime.IAdaptable;
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.MultiStatus;
@@ -47,6 +48,7 @@
 import org.eclipse.core.runtime.Platform;
 import org.eclipse.core.runtime.ProgressMonitorWrapper;
 import org.eclipse.core.runtime.Status;
+import org.eclipse.equinox.internal.p2.artifact.processors.pgp.PGPSignatureVerifier;
 import org.eclipse.equinox.internal.p2.artifact.repository.CompositeArtifactRepository;
 import org.eclipse.equinox.internal.p2.artifact.repository.simple.SimpleArtifactRepository;
 import org.eclipse.equinox.internal.p2.director.ProfileChangeRequest;
@@ -70,6 +72,7 @@
 import org.eclipse.equinox.p2.engine.IPhaseSet;
 import org.eclipse.equinox.p2.engine.IProfile;
 import org.eclipse.equinox.p2.engine.IProvisioningPlan;
+import org.eclipse.equinox.p2.engine.PhaseSetFactory;
 import org.eclipse.equinox.p2.engine.ProvisioningContext;
 import org.eclipse.equinox.p2.engine.query.UserVisibleRootQuery;
 import org.eclipse.equinox.p2.metadata.IArtifactKey;
@@ -88,9 +91,15 @@
 import org.eclipse.equinox.p2.query.IQueryResult;
 import org.eclipse.equinox.p2.query.IQueryable;
 import org.eclipse.equinox.p2.query.QueryUtil;
+import org.eclipse.equinox.p2.repository.IRepository;
+import org.eclipse.equinox.p2.repository.IRepositoryManager;
+import org.eclipse.equinox.p2.repository.artifact.ArtifactDescriptorQuery;
 import org.eclipse.equinox.p2.repository.artifact.IArtifactDescriptor;
 import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository;
+import org.eclipse.equinox.p2.repository.artifact.IArtifactRepositoryManager;
 import org.eclipse.equinox.p2.repository.artifact.IArtifactRequest;
+import org.eclipse.equinox.p2.repository.artifact.IFileArtifactRepository;
+import org.eclipse.equinox.p2.repository.artifact.spi.ArtifactDescriptor;
 import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository;
 import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager;
 import org.eclipse.equinox.p2.repository.spi.PGPPublicKeyService;
@@ -99,9 +108,11 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.net.URI;
+import java.nio.file.Files;
 import java.text.NumberFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -113,6 +124,7 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
@@ -554,6 +566,11 @@
             IEngine engine = agent.getEngine();
             ensureSameBackupDevice(provisioningPlan);
 
+            if (Arrays.asList(phaseSet.getPhaseIds()).contains(PhaseSetFactory.PHASE_CHECK_TRUST))
+            {
+              checkMissingPGPSignatures(agent, artifactURIs, MonitorUtil.create(monitor, 1));
+            }
+
             IStatus status = PlanExecutionHelper.executePlan(provisioningPlan, engine, phaseSet, provisioningContext,
                 new ExecutePlanMonitor(monitor, provisioningPlan));
 
@@ -593,6 +610,124 @@
     }
   }
 
+  private void checkMissingPGPSignatures(Agent agent, Set<URI> artifactURIs, IProgressMonitor monitor)
+  {
+    IProvisioningAgent provisioningAgent = agent.getProvisioningAgent();
+    IArtifactRepositoryManager artifactRepositoryManager = agent.getArtifactRepositoryManager();
+
+    BundlePool bundlePool = profile.getBundlePool();
+    URI bundlePoolLocation = bundlePool.getLocation().toURI();
+
+    // Load the artifact repositories in parallel.
+    Set<URI> uris = new LinkedHashSet<>();
+    uris.add(bundlePoolLocation);
+    uris.addAll(artifactURIs);
+    RepositoryLoader<IArtifactKey> repositoryLoader = new RepositoryLoader<>(artifactRepositoryManager, uris.toArray(new URI[uris.size()]));
+    repositoryLoader.begin(monitor);
+    ProvisionException exception = repositoryLoader.getException();
+    if (exception != null)
+    {
+      // Ignore.
+      return;
+    }
+
+    if (repositoryLoader.isCanceled())
+    {
+      throw new OperationCanceledException();
+    }
+
+    try
+    {
+      List<IQueryable<IArtifactDescriptor>> repositories = new ArrayList<>();
+      for (URI uri : uris)
+      {
+        repositories.add(artifactRepositoryManager.loadRepository(uri, monitor).descriptorQueryable());
+      }
+
+      Map<IArtifactKey, IArtifactDescriptor> pgpArtifacts = new LinkedHashMap<>();
+      for (IArtifactDescriptor descriptor : QueryUtil.compoundQueryable(repositories).query(ArtifactDescriptorQuery.ALL_DESCRIPTORS, monitor))
+      {
+        if (descriptor.getProperty(PGPSignatureVerifier.PGP_SIGNATURES_PROPERTY_NAME) != null)
+        {
+          pgpArtifacts.put(descriptor.getArtifactKey(), descriptor);
+        }
+      }
+
+      IFileArtifactRepository artifactRepository = (IFileArtifactRepository)artifactRepositoryManager.loadRepository(bundlePoolLocation, monitor);
+      List<Runnable> runnables = new ArrayList<>();
+      for (IArtifactDescriptor descriptor : artifactRepository.descriptorQueryable().query(ArtifactDescriptorQuery.ALL_DESCRIPTORS, monitor))
+      {
+        // Look for all descriptors in the bundle pool with without PGP signature...
+        if (descriptor.getProperty(PGPSignatureVerifier.PGP_SIGNATURES_PROPERTY_NAME) == null)
+        {
+          // If one of the other repositories has a PGP signature...
+          IArtifactDescriptor other = pgpArtifacts.get(descriptor.getArtifactKey());
+          if (other != null)
+          {
+            File artifactFile = artifactRepository.getArtifactFile(descriptor);
+            if (artifactFile != null && artifactFile.isFile())
+            {
+              // Defer checking the artifact against the signature to run while the repository is locked.
+              runnables.add(() -> {
+                try (PGPSignatureVerifier pgpSignatureVerifier = new PGPSignatureVerifier())
+                {
+                  // A stream that ignores the writes but provides access to the target descriptor so that it can be updated if the PGP signing is successful.
+                  class NullOutputStream extends OutputStream implements IAdaptable
+                  {
+                    @Override
+                    public <T> T getAdapter(Class<T> adapter)
+                    {
+                      if (adapter == ArtifactDescriptor.class)
+                      {
+                        return adapter.cast(descriptor);
+                      }
+                      return null;
+                    }
+
+                    @Override
+                    public void write(int b) throws IOException
+                    {
+                    }
+
+                    @Override
+                    public void write(byte[] b, int off, int len) throws IOException
+                    {
+                    }
+                  }
+
+                  pgpSignatureVerifier.link(new NullOutputStream(), monitor);
+                  pgpSignatureVerifier.initialize(provisioningAgent, null, other);
+                  Files.copy(artifactFile.toPath(), pgpSignatureVerifier);
+                }
+                catch (IOException ex)
+                {
+                  P2CorePlugin.INSTANCE.log(ex);
+                }
+
+                String property = descriptor.getProperty(PGPSignatureVerifier.PGP_SIGNATURES_PROPERTY_NAME);
+                if (property != null)
+                {
+                  monitor.subTask(NLS.bind(Messages.ProfileTransactionImpl_AddPGPSignature, artifactFile));
+                }
+              });
+            }
+          }
+        }
+      }
+
+      if (!runnables.isEmpty())
+      {
+        artifactRepository.executeBatch(m -> {
+          runnables.parallelStream().forEach(Runnable::run);
+        }, monitor);
+      }
+    }
+    catch (ProvisionException ex)
+    {
+      P2CorePlugin.INSTANCE.log(ex);
+    }
+  }
+
   private void computeOperandDeltas(final IProvisioningPlan provisioningPlan, Map<IInstallableUnit, CommitContext.DeltaType> iuDeltas,
       Map<IInstallableUnit, Map<String, Pair<Object, Object>>> propertyDeltas)
   {
@@ -820,7 +955,7 @@
       }
     }
 
-    RepositoryLoader repositoryLoader = new RepositoryLoader(metadataRepositoryManager, metadataURIs);
+    RepositoryLoader<IInstallableUnit> repositoryLoader = new RepositoryLoader<>(metadataRepositoryManager, metadataURIs);
     repositoryLoader.begin(monitor);
     ProvisionException exception = repositoryLoader.getException();
     if (exception != null)
@@ -1544,17 +1679,17 @@
   /**
    * @author Ed Merks
    */
-  private static final class RepositoryLoader extends WorkerPool<RepositoryLoader, URI, RepositoryLoader.Worker>
+  private static final class RepositoryLoader<T> extends WorkerPool<RepositoryLoader<T>, URI, RepositoryLoader.Worker<T>>
   {
-    private final IMetadataRepositoryManager manager;
+    private final IRepositoryManager<T> manager;
 
     private final URI[] uris;
 
-    private final List<IMetadataRepository> metadataRepositories = Collections.synchronizedList(new ArrayList<IMetadataRepository>());
+    private final List<IRepository<T>> repositories = Collections.synchronizedList(new ArrayList<>());
 
     private ProvisionException exception;
 
-    public RepositoryLoader(IMetadataRepositoryManager manager, URI... uris)
+    public RepositoryLoader(IRepositoryManager<T> manager, URI... uris)
     {
       this.manager = manager;
       this.uris = uris;
@@ -1586,17 +1721,17 @@
     }
 
     @Override
-    protected Worker createWorker(URI key, int workerID, boolean secondary)
+    protected Worker<T> createWorker(URI key, int workerID, boolean secondary)
     {
-      return new Worker(NLS.bind(Messages.ProfileTransactionImpl_RepositoryLoader_thread, key), this, key, workerID, secondary);
+      return new Worker<T>(NLS.bind(Messages.ProfileTransactionImpl_RepositoryLoader_thread, key), this, key, workerID, secondary);
     }
 
     /**
      * @author Ed Merks
      */
-    private static class Worker extends WorkerPool.Worker<URI, RepositoryLoader>
+    private static class Worker<T> extends WorkerPool.Worker<URI, RepositoryLoader<T>>
     {
-      public Worker(String name, RepositoryLoader workPool, URI key, int id, boolean secondary)
+      public Worker(String name, RepositoryLoader<T> workPool, URI key, int id, boolean secondary)
       {
         super(name, workPool, key, id, secondary);
       }
@@ -1604,7 +1739,7 @@
       @Override
       protected IStatus perform(IProgressMonitor monitor)
       {
-        RepositoryLoader workPool = getWorkPool();
+        RepositoryLoader<T> workPool = getWorkPool();
         if (workPool.isCanceled())
         {
           return Status.CANCEL_STATUS;
@@ -1612,8 +1747,13 @@
 
         try
         {
-          IMetadataRepository metadataRepository = workPool.manager.loadRepository(getKey(), MonitorUtil.create(monitor, 1));
-          workPool.metadataRepositories.add(metadataRepository);
+          IRepositoryManager<T> manager = workPool.manager;
+          URI key = getKey();
+          @SuppressWarnings("unchecked")
+          IRepository<T> repository = (IRepository<T>)(manager instanceof IMetadataRepositoryManager
+              ? ((IMetadataRepositoryManager)manager).loadRepository(key, MonitorUtil.create(monitor, 1))
+              : ((IArtifactRepositoryManager)manager).loadRepository(key, MonitorUtil.create(monitor, 1)));
+          workPool.repositories.add(repository);
           return Status.OK_STATUS;
         }
         catch (ProvisionException ex)
diff --git a/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/messages.properties b/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/messages.properties
index e300352..188b361 100644
--- a/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/messages.properties
+++ b/plugins/org.eclipse.oomph.p2.core/src/org/eclipse/oomph/p2/internal/core/messages.properties
@@ -21,9 +21,11 @@
 BundlePoolImpl_CachCannotBeChanged_exception=Cache folder of a pooled profile cannot be changed: {0}
 BundlePoolImpl_PoolNotLoaded_exception=Bundle pool {0} could not be loaded
 BundlePoolImpl_SharedBundlePool_label=Shared Bundle Pool
-CachingRepositoryManager_AddingRepository_task=Adding repository {0}
+CachingRepositoryManager_AddingRepository_task=Adding {0} repository {1}
+CachingRepositoryManager_artifact=artifact
 CachingRepositoryManager_Failure_message=with {0} failures
 CachingRepositoryManager_LoadingFailed_message=Loading ''{0}'' failed. retry={1}
+CachingRepositoryManager_metadata=metadata
 CachingRepositoryManager_NonRelative_message=Unable to make location {0} relative to mirror {1}
 CachingRepositoryManager_RepeatedDownload_task=Repeated attempts to download {0} probably because it can''t be processed
 CachingRepositoryManager_Speed_message=Mirrored {0} artifacts from {1} at {2}kb/s
@@ -46,6 +48,7 @@
 ProfileImpl_ProfileNotExists_exception=Profile does not exist: {0}
 ProfileReferencerImpl_NotDirectory_exception=Not a directory: {0}
 ProfileReferencerImpl_NotFile_exception=Not a file: {0}
+ProfileTransactionImpl_AddPGPSignature=Added a PGP signature for {0}
 ProfileTransactionImpl_CollectingArtifacts_task=Collecting {0} artifacts from {1}
 ProfileTransactionImpl_CollectingArtifactsFor_task=Collected {0} artifacts for {1} in {2}s
 ProfileTransactionImpl_CouldNotBeChanged_message=Profile could not be changed