001/*
002 * Extra Bnd Repository Plugins
003 * Copyright (C) 2019-2022 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package de.mnl.osgi.bnd.repository.maven.idxmvn;
020
021import aQute.bnd.http.HttpClient;
022import aQute.bnd.osgi.repository.ResourcesRepository;
023import aQute.bnd.osgi.repository.XMLResourceGenerator;
024import aQute.bnd.osgi.repository.XMLResourceParser;
025import aQute.bnd.osgi.resource.ResourceUtils;
026import aQute.bnd.version.Version;
027import aQute.maven.api.Archive;
028import aQute.maven.api.Program;
029import aQute.maven.api.Revision;
030import aQute.maven.provider.MavenBackingRepository;
031import aQute.service.reporter.Reporter;
032import de.mnl.osgi.bnd.maven.CompositeMavenRepository.BinaryLocation;
033import de.mnl.osgi.bnd.maven.MavenResource;
034import de.mnl.osgi.bnd.maven.MavenResourceException;
035import de.mnl.osgi.bnd.maven.MavenVersion;
036import de.mnl.osgi.bnd.maven.MavenVersionRange;
037import de.mnl.osgi.bnd.maven.MavenVersionSpecification;
038import static de.mnl.osgi.bnd.maven.RepositoryUtils.rethrow;
039import static de.mnl.osgi.bnd.maven.RepositoryUtils.unthrow;
040import java.io.File;
041import java.io.IOException;
042import java.io.InputStream;
043import java.io.Writer;
044import java.net.URI;
045import java.nio.charset.Charset;
046import java.nio.file.Files;
047import java.nio.file.Path;
048import java.util.ArrayList;
049import java.util.Collection;
050import java.util.Collections;
051import java.util.Comparator;
052import java.util.HashSet;
053import java.util.List;
054import java.util.Map;
055import java.util.Optional;
056import java.util.Properties;
057import java.util.Set;
058import java.util.concurrent.CompletableFuture;
059import java.util.concurrent.ConcurrentHashMap;
060import java.util.concurrent.ConcurrentMap;
061import java.util.concurrent.ExecutionException;
062import java.util.function.Supplier;
063import java.util.regex.Matcher;
064import java.util.regex.Pattern;
065import java.util.stream.Collectors;
066import org.apache.maven.model.Dependency;
067import org.osgi.resource.Capability;
068import org.osgi.resource.Resource;
069import org.slf4j.Logger;
070import org.slf4j.LoggerFactory;
071
072/**
073 * A repository with artifacts from a single group.
074 */
075@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyFields",
076    "PMD.ExcessiveImports", "PMD.GodClass", "PMD.TooManyMethods",
077    "PMD.CyclomaticComplexity" })
078public class MavenGroupRepository extends ResourcesRepository {
079
080    private static final Logger LOG = LoggerFactory.getLogger(
081        MavenGroupRepository.class);
082
083    /** Indexing state. */
084    private enum IndexingState {
085        NONE, CHECKING, INDEXED, EXCLUDED, EXCL_BY_DEP
086    }
087
088    private final String groupId;
089    private boolean requested;
090    private final IndexedMavenRepository indexedRepository;
091    private final HttpClient client;
092    private final Reporter reporter;
093    private Path groupDir;
094    private Path groupPropsPath;
095    private Path groupIndexPath;
096    private final Properties groupProps;
097    private VersionSpecification[] versionSpecs = new VersionSpecification[0];
098    private final ConcurrentMap<Archive, IndexingState> indexingState
099        = new ConcurrentHashMap<>();
100    private ResourcesRepository backupRepo;
101    private Writer indexingLog;
102    private final Map<Revision, List<String>> loggedMessages
103        = new ConcurrentHashMap<>();
104
105    @SuppressWarnings("PMD.FieldNamingConventions")
106    private static final Pattern hrefPattern = Pattern.compile(
107        "<[aA]\\s+(?:[^>]*?\\s+)?href=(?<quote>[\"'])"
108            + ":?(?<href>[a-zA-Z].*?)\\k<quote>");
109
110    /**
111     * Instantiates a new representation of group data backed
112     * by the specified directory. 
113     *
114     * @param groupId the maven groupId indexed by this repository
115     * @param directory the directory used to persist data
116     * @param requested if it is a requested group id
117     * @param indexedRepository the indexed maven repository
118     * @param client the client used for remote access
119     * @param reporter the reporter
120     * @throws IOException Signals that an I/O exception has occurred.
121     */
122    @SuppressWarnings({ "PMD.ConfusingTernary",
123        "PMD.AvoidCatchingGenericException", "PMD.AvoidDuplicateLiterals",
124        "PMD.GuardLogStatement" })
125    public MavenGroupRepository(String groupId, Path directory,
126            boolean requested, IndexedMavenRepository indexedRepository,
127            HttpClient client, Reporter reporter) throws IOException {
128        this.groupId = groupId;
129        this.requested = requested;
130        this.indexedRepository = indexedRepository;
131        this.client = client;
132        this.reporter = reporter;
133
134        // Prepare directory and files
135        updatePaths(directory);
136        groupProps = new Properties();
137        if (groupPropsPath.toFile().canRead()) {
138            try (InputStream input = Files.newInputStream(groupPropsPath)) {
139                groupProps.load(input);
140                versionSpecs = VersionSpecification.parse(groupProps);
141            }
142        }
143
144        // Prepare OSGi repository
145        if (groupIndexPath.toFile().canRead()) {
146            try (XMLResourceParser parser
147                = new XMLResourceParser(groupIndexPath.toFile())) {
148                addAll(parser.parse());
149            } catch (Exception e) { // NOPMD
150                reporter.warning("Cannot parse %s, ignored: %s", groupIndexPath,
151                    e.getMessage());
152            }
153        }
154
155        // Re-use exiting resources for faster checks.
156        for (Capability cap : findProvider(
157            newRequirementBuilder("bnd.info").build())) {
158            indexingState.put(
159                Archive.valueOf((String) cap.getAttributes().get("from")),
160                IndexingState.INDEXED);
161        }
162        LOG.debug("Created group repository for {}.", groupId);
163    }
164
165    private void updatePaths(Path directory) {
166        if (!directory.toFile().exists()) {
167            directory.toFile().mkdir();
168        }
169        groupDir = directory;
170        groupPropsPath = groupDir.resolve("group.properties");
171        groupIndexPath = groupDir.resolve("index.xml");
172    }
173
174    /**
175     * Checks if is requested.
176     *
177     * @return the requested
178     */
179    public final boolean isRequested() {
180        return requested;
181    }
182
183    /**
184     * Writes all changes to persistent storage and removes
185     * any backup information prepared by {@link #reset(Path, boolean)}.
186     *
187     * @throws IOException Signals that an I/O exception has occurred.
188     */
189    public void flush() throws IOException {
190        boolean indexChanged = true;
191        if (backupRepo != null) {
192            Set<Resource> oldSet = new HashSet<>(backupRepo.getResources());
193            Set<Resource> newSet = new HashSet<>(getResources());
194            if (newSet.equals(oldSet)) {
195                indexChanged = false;
196            }
197        }
198        if (indexChanged) {
199            XMLResourceGenerator generator = new XMLResourceGenerator();
200            generator.resources(getResources());
201            generator.name(indexedRepository.mavenRepository().name());
202            try {
203                generator.save(groupIndexPath.toFile());
204            } catch (IOException e) {
205                reporter.exception(e, "Cannot save %s.", groupIndexPath);
206            }
207        }
208        backupRepo = null;
209        if (indexingLog != null) {
210            rethrow(IOException.class, () -> loggedMessages.entrySet().stream()
211                .sorted(Map.Entry.comparingByKey())
212                .flatMap(e -> e.getValue().stream())
213                .forEach(msg -> unthrow(() -> indexingLog
214                    .write(msg + System.lineSeparator()))));
215            indexingLog.close();
216            indexingLog = null;
217        }
218        loggedMessages.clear();
219        indexingState.clear();
220    }
221
222    /**
223     * Removes the group directory if empty.
224     *
225     * @return true, if removed
226     * @throws IOException Signals that an I/O exception has occurred.
227     */
228    public boolean removeIfRedundant() throws IOException {
229        if (isRequested() || !groupProps.isEmpty()
230            || !getResources().isEmpty()) {
231            return false;
232        }
233        // Nothing in this group
234        Files.walk(groupDir)
235            .sorted(Comparator.reverseOrder())
236            .map(Path::toFile)
237            .forEach(File::delete);
238        return true;
239    }
240
241    /**
242     * Returns the group id.
243     *
244     * @return the groupId
245     */
246    @SuppressWarnings("PMD.ShortMethodName")
247    public final String id() {
248        return groupId;
249    }
250
251    /**
252     * Clears this repository and updates the path and the requested 
253     * flag. Keeps the current content as backup for reuse in a 
254     * subsequent call to {@link #reload()}.
255     *
256     * @param directory this group's directory
257     * @param requested whether this is a requested group
258     */
259    @SuppressWarnings({ "PMD.ConfusingTernary", "PMD.GuardLogStatement" })
260    public void reset(Path directory, boolean requested) {
261        synchronized (this) {
262            // Update basic properties
263            this.requested = requested;
264            if (!groupDir.equals(directory)) {
265                updatePaths(directory);
266                backupRepo = null;
267            } else {
268                // Save current content and clear.
269                backupRepo = new ResourcesRepository(getResources());
270            }
271            set(Collections.emptyList());
272            // Clear and reload properties
273            groupProps.clear();
274            if (groupPropsPath.toFile().canRead()) {
275                try (InputStream input = Files.newInputStream(groupPropsPath)) {
276                    groupProps.load(input);
277                    versionSpecs = VersionSpecification.parse(groupProps);
278                } catch (IOException e) {
279                    reporter.warning("Problem reading %s (ignored): %s",
280                        groupPropsPath, e.getMessage());
281                }
282            }
283            // Clear remaining caches.
284            indexingState.clear();
285        }
286    }
287
288    /**
289     * Reset the repository group. Must be called for all groups before
290     * reloading. 
291     *
292     * @throws IOException Signals that an I/O exception has occurred.
293     */
294    /* package */ void reset() throws IOException {
295        if (indexedRepository.logIndexing()) {
296            indexingLog = Files.newBufferedWriter(
297                groupDir.resolve("indexing.log"), Charset.defaultCharset());
298        } else {
299            // Don't keep out-dated log files, it's irritating.
300            groupDir.resolve("indexing.log").toFile().delete();
301        }
302        loggedMessages.clear();
303
304        if (!isRequested()) {
305            // Will be filled with dependencies only
306            return;
307        }
308        // Actively filled.
309        synchronized (this) {
310            if (backupRepo == null) {
311                backupRepo = new ResourcesRepository(getResources());
312            }
313        }
314    }
315
316    /**
317     * Reload the repository. May be called concurrently for different
318     * group repositories after resetting all. Requested repositories 
319     * retrieve the list of known artifactIds from the remote repository 
320     * and add the versions. For versions already in the repository, 
321     * the backup information is re-used.
322     *
323     * @throws IOException Signals that an I/O exception has occurred.
324     */
325    @SuppressWarnings({ "PMD.AvoidReassigningLoopVariables",
326        "PMD.AvoidCatchingGenericException", "PMD.AvoidDuplicateLiterals",
327        "PMD.AvoidInstantiatingObjectsInLoops",
328        "PMD.AvoidThrowingRawExceptionTypes", "PMD.PreserveStackTrace" })
329    /* package */ void reload() throws IOException {
330        if (!isRequested()) {
331            // Will be filled with dependencies only
332            return;
333        }
334        try {
335            CompletableFuture<?>[] programLoaders = findArtifactIds().stream()
336                .map(artifactId -> loadProgram(
337                    Program.valueOf(groupId, artifactId)))
338                .toArray(CompletableFuture[]::new);
339            CompletableFuture.allOf(programLoaders).get();
340        } catch (ExecutionException e) {
341            if (e.getCause() instanceof IOException) {
342                throw (IOException) e.getCause();
343            }
344            throw new IOException(e.getCause());
345        } catch (InterruptedException e) {
346            reporter.exception(e, "Loading %s has been interrupted: %s",
347                groupId, e.getMessage());
348            throw new RuntimeException(e);
349        }
350    }
351
352    @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
353        "PMD.AvoidInstantiatingObjectsInLoops", "PMD.GuardLogStatement" })
354    private Collection<String> findArtifactIds() {
355        Set<String> result = new HashSet<>();
356        for (MavenBackingRepository repo : indexedRepository.mavenRepository()
357            .backing()) {
358            URI groupUri = null;
359            try {
360                groupUri
361                    = repo.toURI("").resolve(groupId.replace('.', '/') + "/");
362                String page = client.build().headers("User-Agent", "Bnd")
363                    .get(String.class)
364                    .go(groupUri);
365                if (page == null) {
366                    continue;
367                }
368                Matcher matcher = hrefPattern.matcher(page);
369                while (matcher.find()) {
370                    URI programUri = groupUri.resolve(matcher.group("href"));
371                    String artifactId = programUri.getPath()
372                        .substring(groupUri.getPath().length());
373                    if (artifactId.endsWith("/")) {
374                        artifactId
375                            = artifactId.substring(0, artifactId.length() - 1);
376                    }
377                    result.add(artifactId);
378                }
379            } catch (Exception e) {
380                reporter.warning("Problem retrieving %s, skipped: %s", groupUri,
381                    e.getMessage());
382            }
383        }
384        return result;
385    }
386
387    @SuppressWarnings({ "PMD.AvoidCatchingThrowable", "PMD.CognitiveComplexity",
388        "PMD.NPathComplexity", "PMD.NcssCount" })
389    private CompletableFuture<Void> loadProgram(Program program) {
390        // Get revisions of program and process.
391        CompletableFuture<Void> result = new CompletableFuture<>();
392        IndexedMavenRepository.programLoaders.submit(() -> {
393            String threadName = Thread.currentThread().getName();
394            try {
395                Thread.currentThread().setName("RevisionQuerier " + program);
396                var resources = listRevisions(program);
397                if (resources.isEmpty()) {
398                    return;
399                }
400                removeOutOfOrderVersions(resources);
401
402                // Now start indexing for remaining
403                for (var resource : resources) {
404                    var archive = resource.archive();
405                    // Indexing may have been started for this as dependency
406                    // but as we don't know yet if that will be successful,
407                    // we start it nevertheless (concurrently).
408                    if (Optional.ofNullable(indexingState.putIfAbsent(
409                        resource.archive(), IndexingState.CHECKING))
410                        .orElse(
411                            IndexingState.CHECKING) != IndexingState.CHECKING) {
412                        logIndexing(resource, () -> String.format(
413                            "%s from revision list already handled as dependency.",
414                            resource));
415                        continue;
416                    }
417                    logIndexing(resource, () -> String.format(
418                        "%s in revision list, indexing...", resource));
419                    var deps = indexableDependencies(resource, true);
420                    if (deps == null) {
421                        if (indexingState.replace(archive,
422                            IndexingState.CHECKING,
423                            IndexingState.EXCL_BY_DEP)) {
424                            logIndexing(archive, () -> String.format(
425                                "%s skipped due to unavailable "
426                                    + "dependencies.",
427                                archive));
428                        }
429                        continue;
430                    }
431                    addResourceAndDependencies(resource, deps);
432                }
433            } finally {
434                Thread.currentThread().setName(threadName);
435                result.complete(null);
436            }
437        });
438        return result;
439    }
440
441    private List<MavenResource> listRevisions(Program program) {
442        return indexedRepository.mavenRepository().findRevisions(program)
443            .flatMap(revision -> {
444                var boundArchives
445                    = VersionSpecification.toSelected(versionSpecs, revision);
446                if (boundArchives.isEmpty()) {
447                    logIndexing(revision.unbound(),
448                        () -> String.format("%s not selected for indexing.",
449                            revision.unbound()));
450                }
451                return boundArchives.stream();
452            }).map(boundArchive -> {
453                LOG.debug("Loading archive {}.", boundArchive);
454                return indexedRepository.mavenRepository()
455                    .resource(boundArchive, BinaryLocation.REMOTE);
456            }).sorted(new Comparator<>() {
457                @Override
458                public int compare(MavenResource res1, MavenResource res2) {
459                    // Sort descending
460                    return res2.archive().compareTo(res1.archive());
461                }
462            }).collect(Collectors.toList());
463    }
464
465    private void removeOutOfOrderVersions(List<MavenResource> resources) {
466        // Remove resources with versions that are inconsistent
467        // with OSGi version order.
468        var resourcesIter = resources.iterator();
469        Version lastVersion = null;
470        while (resourcesIter.hasNext()) {
471            var next = resourcesIter.next();
472            var nextVersion = osgiVersion(next);
473            if (nextVersion.isEmpty()) {
474                continue;
475            }
476            if (lastVersion != null
477                && nextVersion.get().compareTo(lastVersion) >= 0) {
478                resourcesIter.remove();
479                logIndexing(next, () -> String.format(
480                    "%s skipped, violates OSGi version order.", next));
481                continue;
482            }
483            lastVersion = nextVersion.get();
484        }
485    }
486
487    /**
488     * Checks if the resource matches the selection criteria.
489     *
490     * @param resource the resource to check
491     * @return true, if the revision matches
492     */
493    @SuppressWarnings("PMD.CollapsibleIfStatements")
494    private boolean indexingCandidate(MavenResource resource) {
495        Archive archive = resource.archive();
496        if (!archive.revision.group.equals(groupId)) {
497            throw new IllegalArgumentException("Wrong groupId "
498                + archive.revision.group + " (must be " + groupId + ").");
499        }
500        // Check if forced.
501        if (VersionSpecification.isForced(versionSpecs, archive)) {
502            return true;
503        }
504        // Check if excluded by rule.
505        if (VersionSpecification
506            .excluded(versionSpecs, archive.revision.artifact)
507            .includes(MavenVersion.from(archive.revision.version))) {
508            if (indexingState.replace(archive, IndexingState.CHECKING,
509                IndexingState.EXCLUDED)) {
510                logIndexing(archive.revision,
511                    () -> String.format("%s is excluded by rule.", archive));
512            }
513            return false;
514        }
515        return true;
516    }
517
518    @SuppressWarnings("PMD.AvoidCatchingGenericException")
519    private void addResourceAndDependencies(MavenResource resource,
520            Set<MavenResource> allDeps) {
521        addResource(resource);
522        // Add the dependencies found while checking to the index.
523        for (var depRes : allDeps) {
524            try {
525                indexedRepository.getOrCreateGroupRepository(
526                    depRes.archive().getRevision().group).addResource(depRes);
527            } catch (IOException e) {
528                // No reason to fail completely.
529                reporter.exception(e, "Failed to add dependency %s of %s: %s",
530                    depRes, resource, e.getMessage());
531                logIndexing(resource, () -> String.format(
532                    "%s failed to add depedendency %s.", resource, depRes));
533            }
534        }
535    }
536
537    private Optional<Version> osgiVersion(MavenResource resource) {
538        try {
539            if (ResourceUtils.getIdentityCapability(
540                resource.asResource()) == null) {
541                return Optional.empty();
542            }
543            return Optional
544                .ofNullable(ResourceUtils.getVersion(resource.asResource()));
545        } catch (IllegalArgumentException | MavenResourceException e) {
546            reporter.exception(e, "Failed to get as resource %s: %s",
547                resource.archive(), e.getMessage());
548            logIndexing(resource,
549                () -> String.format("%s failed to load.", resource));
550            return Optional.empty();
551        }
552    }
553
554    @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
555        "PMD.ReturnEmptyCollectionRatherThanNull" })
556    private Set<MavenResource> indexableDependencies(MavenResource resource,
557            boolean log) {
558        // Get dependencies and check them
559        List<Dependency> dependencies = evaluateDependencies(resource);
560        if (!dependencies.isEmpty() && log) {
561            logIndexing(resource,
562                () -> String.format("%s has dependencies: %s",
563                    resource, dependencies.stream()
564                        .map(d -> d.getGroupId() + ":" + d.getArtifactId() + ":"
565                            + d.getVersion())
566                        .collect(Collectors.joining(", "))));
567        }
568        Set<MavenResource> indexable = new HashSet<>();
569        boolean isForced = VersionSpecification.isForced(versionSpecs,
570            resource.archive());
571        for (Dependency dep : dependencies) {
572            MavenGroupRepository depRepo;
573            try {
574                depRepo = indexedRepository
575                    .getOrCreateGroupRepository(dep.getGroupId());
576            } catch (Exception e) {
577                reporter.exception(e, "Failed to get repo %s: %s",
578                    dep.getGroupId(), e.getMessage());
579                // Failing to get a dependency is no reason to fail.
580                continue;
581            }
582            var depsDeps = depRepo.collectTransient(resource, dep, isForced);
583            if (depsDeps == null) {
584                if (log) {
585                    logIndexing(resource, () -> String.format(
586                        "%s lacks dependency: %s", resource,
587                        dep.getGroupId() + ":" + dep.getArtifactId() + ":"
588                            + dep.getVersion()));
589                }
590                return null;
591            }
592            indexable.addAll(depsDeps);
593        }
594        return indexable;
595    }
596
597    @SuppressWarnings({ "PMD.CollapsibleIfStatements",
598        "PMD.ReturnEmptyCollectionRatherThanNull", "PMD.NcssCount",
599        "PMD.CognitiveComplexity" })
600    private Set<MavenResource> collectTransient(MavenResource dependant,
601            Dependency dependency, boolean dontFail) {
602        Set<MavenResource> collected = new HashSet<>();
603        Optional<MavenResource> optRes = dependencyToResource(dependency);
604        if (!optRes.isPresent()) {
605            // Failing to get the resource is no reason to fail.
606            return collected;
607        }
608        MavenResource resource = optRes.get();
609        IndexingState state = Optional.ofNullable(indexingState
610            .putIfAbsent(resource.archive(), IndexingState.CHECKING))
611            .orElse(IndexingState.NONE);
612        switch (state) {
613        case INDEXED:
614            // Dependency (and its dependencies() have already been indexed.
615            return collected;
616        case EXCLUDED:
617            if (dontFail) {
618                return collected;
619            }
620            logIndexing(resource, () -> String.format(
621                "%s is excluded, thus blocks %s.", resource,
622                dependant));
623            return null;
624        case EXCL_BY_DEP:
625            // Indexing of dependency has already failed.
626            if (dontFail) {
627                return collected;
628            }
629            logIndexing(resource, () -> String.format(
630                "%s lacks dependencies, thus blocks %s.", resource,
631                dependant));
632            return null;
633        case NONE:
634            // Only the first attempt reports.
635            logIndexing(resource, () -> String.format(
636                "%s is checked as dependency of %s...", resource, dependant));
637            break;
638        default:
639            break;
640        }
641
642        // Attempt to index.
643        Set<MavenResource> transDeps = null;
644        boolean candidate = indexingCandidate(resource);
645        if (candidate || dontFail) {
646            transDeps = indexableDependencies(resource,
647                state == IndexingState.NONE);
648        }
649        if (transDeps == null) {
650            // Note that the revision which was checked is not indexable
651            // due to a dependency that is not indexable (unless forced)
652            if (!dontFail) {
653                if (indexingState.replace(resource.archive(),
654                    IndexingState.CHECKING, IndexingState.EXCL_BY_DEP)) {
655                    logIndexing(resource, () -> String.format(
656                        "%s lacks dependencies, thus blocks %s.", resource,
657                        dependant));
658                }
659                return null;
660            }
661        } else {
662            collected.addAll(transDeps);
663        }
664        collected.add(resource);
665        return collected;
666    }
667
668    @SuppressWarnings("PMD.AvoidCatchingGenericException")
669    private List<Dependency> evaluateDependencies(MavenResource resource) {
670        List<Dependency> deps;
671        try {
672            deps = resource.dependencies();
673        } catch (Exception e) {
674            reporter.exception(e, "Failed to get dependency of %s: %s",
675                resource, e.getMessage());
676            logIndexing(resource, () -> String.format(
677                "Failed to get dependencies of %s: %s", resource,
678                e.getMessage()));
679            // Failing to get the dependencies is no reason to fail.
680            return Collections.emptyList();
681        }
682        return deps;
683    }
684
685    @SuppressWarnings("PMD.AvoidCatchingGenericException")
686    private Optional<MavenResource> dependencyToResource(Dependency dep) {
687        Program depPgm = Program.valueOf(dep.getGroupId(), dep.getArtifactId());
688        try {
689            return indexedRepository.mavenRepository().resource(
690                depPgm, narrowVersion(depPgm, MavenVersionSpecification
691                    .from(dep.getVersion())),
692                dep.getType(), dep.getClassifier(),
693                BinaryLocation.REMOTE);
694        } catch (Exception e) {
695            reporter.exception(e, "Failed to get resource %s: %s",
696                depPgm, e.getMessage());
697            // Failing to get a dependency is no reason to fail.
698            return Optional.empty();
699        }
700    }
701
702    private MavenVersionSpecification narrowVersion(
703            Program program, MavenVersionSpecification version)
704            throws IOException {
705        if (version instanceof MavenVersion) {
706            // Specific version, leave as is.
707            return version;
708        }
709        // If it's a range, restrict it to allowed
710        MavenVersionRange excluded = VersionSpecification
711            .excluded(versionSpecs, program.artifact);
712        return excluded.complement().restrict((MavenVersionRange) version);
713    }
714
715    /**
716     * Adds the specified revision.
717     *
718     * @param revision the revision to add
719     */
720    @SuppressWarnings("PMD.AvoidCatchingGenericException")
721    private void addResource(MavenResource resource) {
722        if (!indexingState.replace(resource.archive(),
723            IndexingState.CHECKING, IndexingState.INDEXED)) {
724            return;
725        }
726        try {
727            // The ResourcesRepoitory that we inherit from isn't thread safe
728            synchronized (this) {
729                add(resource.asResource());
730            }
731            logIndexing(resource,
732                () -> String.format("%s added to index.", resource));
733        } catch (Exception e) {
734            reporter.exception(e, "Failed to get %s as resource.", resource);
735            logIndexing(resource, () -> String.format(
736                "%s could not be indexed: %s.", resource, e.getMessage()));
737        }
738    }
739
740    /* package */ Optional<Resource> searchInBackup(Archive archive) {
741        if (backupRepo == null) {
742            return Optional.empty();
743        }
744        return backupRepo.findProvider(
745            backupRepo.newRequirementBuilder("bnd.info")
746                .addDirective("filter",
747                    String.format("(from=%s)", archive.toString()))
748                .build())
749            .stream().findFirst().map(Capability::getResource);
750    }
751
752    private void logIndexing(Revision revision, Supplier<String> msgSupplier) {
753        loggedMessages
754            .computeIfAbsent(revision,
755                rev -> Collections.synchronizedList(new ArrayList<>()))
756            .add(msgSupplier.get());
757    }
758
759    private void logIndexing(Archive archive, Supplier<String> msgSupplier) {
760        logIndexing(archive.revision, msgSupplier);
761    }
762
763    private void logIndexing(MavenResource resource,
764            Supplier<String> msgSupplier) {
765        logIndexing(resource.archive(), msgSupplier);
766    }
767
768    /*
769     * (non-Javadoc)
770     * 
771     * @see java.lang.Object#toString()
772     */
773    @Override
774    public String toString() {
775        return "MavenGroupRepository [groupId=" + groupId + "]";
776    }
777}