001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.internal.impl;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023import javax.inject.Singleton;
024
025import java.io.File;
026import java.util.ArrayList;
027import java.util.Collection;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.concurrent.Executor;
033
034import org.eclipse.aether.RepositoryEvent;
035import org.eclipse.aether.RepositoryEvent.EventType;
036import org.eclipse.aether.RepositorySystemSession;
037import org.eclipse.aether.RequestTrace;
038import org.eclipse.aether.SyncContext;
039import org.eclipse.aether.impl.MetadataResolver;
040import org.eclipse.aether.impl.OfflineController;
041import org.eclipse.aether.impl.RemoteRepositoryFilterManager;
042import org.eclipse.aether.impl.RemoteRepositoryManager;
043import org.eclipse.aether.impl.RepositoryConnectorProvider;
044import org.eclipse.aether.impl.RepositoryEventDispatcher;
045import org.eclipse.aether.impl.UpdateCheck;
046import org.eclipse.aether.impl.UpdateCheckManager;
047import org.eclipse.aether.metadata.Metadata;
048import org.eclipse.aether.repository.ArtifactRepository;
049import org.eclipse.aether.repository.LocalMetadataRegistration;
050import org.eclipse.aether.repository.LocalMetadataRequest;
051import org.eclipse.aether.repository.LocalMetadataResult;
052import org.eclipse.aether.repository.LocalRepository;
053import org.eclipse.aether.repository.LocalRepositoryManager;
054import org.eclipse.aether.repository.RemoteRepository;
055import org.eclipse.aether.repository.RepositoryPolicy;
056import org.eclipse.aether.resolution.MetadataRequest;
057import org.eclipse.aether.resolution.MetadataResult;
058import org.eclipse.aether.spi.connector.MetadataDownload;
059import org.eclipse.aether.spi.connector.RepositoryConnector;
060import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
061import org.eclipse.aether.spi.locator.Service;
062import org.eclipse.aether.spi.locator.ServiceLocator;
063import org.eclipse.aether.spi.synccontext.SyncContextFactory;
064import org.eclipse.aether.transfer.MetadataNotFoundException;
065import org.eclipse.aether.transfer.MetadataTransferException;
066import org.eclipse.aether.transfer.NoRepositoryConnectorException;
067import org.eclipse.aether.transfer.RepositoryOfflineException;
068import org.eclipse.aether.util.concurrency.ExecutorUtils;
069import org.eclipse.aether.util.concurrency.RunnableErrorForwarder;
070
071import static java.util.Objects.requireNonNull;
072
073/**
074 */
075@Singleton
076@Named
077public class DefaultMetadataResolver implements MetadataResolver, Service {
078
079    private static final String CONFIG_PROP_THREADS = "aether.metadataResolver.threads";
080
081    private RepositoryEventDispatcher repositoryEventDispatcher;
082
083    private UpdateCheckManager updateCheckManager;
084
085    private RepositoryConnectorProvider repositoryConnectorProvider;
086
087    private RemoteRepositoryManager remoteRepositoryManager;
088
089    private SyncContextFactory syncContextFactory;
090
091    private OfflineController offlineController;
092
093    private RemoteRepositoryFilterManager remoteRepositoryFilterManager;
094
095    @Deprecated
096    public DefaultMetadataResolver() {
097        // enables default constructor
098    }
099
100    @Inject
101    public DefaultMetadataResolver(
102            RepositoryEventDispatcher repositoryEventDispatcher,
103            UpdateCheckManager updateCheckManager,
104            RepositoryConnectorProvider repositoryConnectorProvider,
105            RemoteRepositoryManager remoteRepositoryManager,
106            SyncContextFactory syncContextFactory,
107            OfflineController offlineController,
108            RemoteRepositoryFilterManager remoteRepositoryFilterManager) {
109        setRepositoryEventDispatcher(repositoryEventDispatcher);
110        setUpdateCheckManager(updateCheckManager);
111        setRepositoryConnectorProvider(repositoryConnectorProvider);
112        setRemoteRepositoryManager(remoteRepositoryManager);
113        setSyncContextFactory(syncContextFactory);
114        setOfflineController(offlineController);
115        setRemoteRepositoryFilterManager(remoteRepositoryFilterManager);
116    }
117
118    public void initService(ServiceLocator locator) {
119        setRepositoryEventDispatcher(locator.getService(RepositoryEventDispatcher.class));
120        setUpdateCheckManager(locator.getService(UpdateCheckManager.class));
121        setRepositoryConnectorProvider(locator.getService(RepositoryConnectorProvider.class));
122        setRemoteRepositoryManager(locator.getService(RemoteRepositoryManager.class));
123        setSyncContextFactory(locator.getService(SyncContextFactory.class));
124        setOfflineController(locator.getService(OfflineController.class));
125        setRemoteRepositoryFilterManager(locator.getService(RemoteRepositoryFilterManager.class));
126    }
127
128    public DefaultMetadataResolver setRepositoryEventDispatcher(RepositoryEventDispatcher repositoryEventDispatcher) {
129        this.repositoryEventDispatcher =
130                requireNonNull(repositoryEventDispatcher, "repository event dispatcher cannot be null");
131        return this;
132    }
133
134    public DefaultMetadataResolver setUpdateCheckManager(UpdateCheckManager updateCheckManager) {
135        this.updateCheckManager = requireNonNull(updateCheckManager, "update check manager cannot be null");
136        return this;
137    }
138
139    public DefaultMetadataResolver setRepositoryConnectorProvider(
140            RepositoryConnectorProvider repositoryConnectorProvider) {
141        this.repositoryConnectorProvider =
142                requireNonNull(repositoryConnectorProvider, "repository connector provider cannot be null");
143        return this;
144    }
145
146    public DefaultMetadataResolver setRemoteRepositoryManager(RemoteRepositoryManager remoteRepositoryManager) {
147        this.remoteRepositoryManager =
148                requireNonNull(remoteRepositoryManager, "remote repository provider cannot be null");
149        return this;
150    }
151
152    public DefaultMetadataResolver setSyncContextFactory(SyncContextFactory syncContextFactory) {
153        this.syncContextFactory = requireNonNull(syncContextFactory, "sync context factory cannot be null");
154        return this;
155    }
156
157    public DefaultMetadataResolver setOfflineController(OfflineController offlineController) {
158        this.offlineController = requireNonNull(offlineController, "offline controller cannot be null");
159        return this;
160    }
161
162    public DefaultMetadataResolver setRemoteRepositoryFilterManager(
163            RemoteRepositoryFilterManager remoteRepositoryFilterManager) {
164        this.remoteRepositoryFilterManager =
165                requireNonNull(remoteRepositoryFilterManager, "remote repository filter manager cannot be null");
166        return this;
167    }
168
169    public List<MetadataResult> resolveMetadata(
170            RepositorySystemSession session, Collection<? extends MetadataRequest> requests) {
171        requireNonNull(session, "session cannot be null");
172        requireNonNull(requests, "requests cannot be null");
173        try (SyncContext shared = syncContextFactory.newInstance(session, true);
174                SyncContext exclusive = syncContextFactory.newInstance(session, false)) {
175            Collection<Metadata> metadata = new ArrayList<>(requests.size());
176            for (MetadataRequest request : requests) {
177                metadata.add(request.getMetadata());
178            }
179
180            return resolve(shared, exclusive, metadata, session, requests);
181        }
182    }
183
184    @SuppressWarnings("checkstyle:methodlength")
185    private List<MetadataResult> resolve(
186            SyncContext shared,
187            SyncContext exclusive,
188            Collection<Metadata> subjects,
189            RepositorySystemSession session,
190            Collection<? extends MetadataRequest> requests) {
191        SyncContext current = shared;
192        try {
193            while (true) {
194                current.acquire(null, subjects);
195
196                final List<MetadataResult> results = new ArrayList<>(requests.size());
197                final List<ResolveTask> tasks = new ArrayList<>(requests.size());
198                final Map<File, Long> localLastUpdates = new HashMap<>();
199                final RemoteRepositoryFilter remoteRepositoryFilter =
200                        remoteRepositoryFilterManager.getRemoteRepositoryFilter(session);
201
202                for (MetadataRequest request : requests) {
203                    RequestTrace trace = RequestTrace.newChild(request.getTrace(), request);
204
205                    MetadataResult result = new MetadataResult(request);
206                    results.add(result);
207
208                    Metadata metadata = request.getMetadata();
209                    RemoteRepository repository = request.getRepository();
210
211                    if (repository == null) {
212                        LocalRepository localRepo =
213                                session.getLocalRepositoryManager().getRepository();
214
215                        metadataResolving(session, trace, metadata, localRepo);
216
217                        File localFile = getLocalFile(session, metadata);
218
219                        if (localFile != null) {
220                            metadata = metadata.setFile(localFile);
221                            result.setMetadata(metadata);
222                        } else {
223                            result.setException(new MetadataNotFoundException(metadata, localRepo));
224                        }
225
226                        metadataResolved(session, trace, metadata, localRepo, result.getException());
227                        continue;
228                    }
229
230                    if (remoteRepositoryFilter != null) {
231                        RemoteRepositoryFilter.Result filterResult =
232                                remoteRepositoryFilter.acceptMetadata(repository, metadata);
233                        if (!filterResult.isAccepted()) {
234                            result.setException(
235                                    new MetadataNotFoundException(metadata, repository, filterResult.reasoning()));
236                            continue;
237                        }
238                    }
239
240                    List<RemoteRepository> repositories =
241                            getEnabledSourceRepositories(repository, metadata.getNature());
242
243                    if (repositories.isEmpty()) {
244                        continue;
245                    }
246
247                    metadataResolving(session, trace, metadata, repository);
248                    LocalRepositoryManager lrm = session.getLocalRepositoryManager();
249                    LocalMetadataRequest localRequest =
250                            new LocalMetadataRequest(metadata, repository, request.getRequestContext());
251                    LocalMetadataResult lrmResult = lrm.find(session, localRequest);
252
253                    File metadataFile = lrmResult.getFile();
254
255                    try {
256                        Utils.checkOffline(session, offlineController, repository);
257                    } catch (RepositoryOfflineException e) {
258                        if (metadataFile != null) {
259                            metadata = metadata.setFile(metadataFile);
260                            result.setMetadata(metadata);
261                        } else {
262                            String msg = "Cannot access " + repository.getId() + " (" + repository.getUrl()
263                                    + ") in offline mode and the metadata " + metadata
264                                    + " has not been downloaded from it before";
265                            result.setException(new MetadataNotFoundException(metadata, repository, msg, e));
266                        }
267
268                        metadataResolved(session, trace, metadata, repository, result.getException());
269                        continue;
270                    }
271
272                    Long localLastUpdate = null;
273                    if (request.isFavorLocalRepository()) {
274                        File localFile = getLocalFile(session, metadata);
275                        localLastUpdate = localLastUpdates.get(localFile);
276                        if (localLastUpdate == null) {
277                            localLastUpdate = localFile != null ? localFile.lastModified() : 0;
278                            localLastUpdates.put(localFile, localLastUpdate);
279                        }
280                    }
281
282                    List<UpdateCheck<Metadata, MetadataTransferException>> checks = new ArrayList<>();
283                    Exception exception = null;
284                    for (RemoteRepository repo : repositories) {
285                        UpdateCheck<Metadata, MetadataTransferException> check = new UpdateCheck<>();
286                        check.setLocalLastUpdated((localLastUpdate != null) ? localLastUpdate : 0);
287                        check.setItem(metadata);
288
289                        // use 'main' installation file for the check (-> use requested repository)
290                        File checkFile = new File(
291                                session.getLocalRepository().getBasedir(),
292                                session.getLocalRepositoryManager()
293                                        .getPathForRemoteMetadata(metadata, repository, request.getRequestContext()));
294                        check.setFile(checkFile);
295                        check.setRepository(repository);
296                        check.setAuthoritativeRepository(repo);
297                        check.setPolicy(
298                                getPolicy(session, repo, metadata.getNature()).getUpdatePolicy());
299
300                        if (lrmResult.isStale()) {
301                            checks.add(check);
302                        } else {
303                            updateCheckManager.checkMetadata(session, check);
304                            if (check.isRequired()) {
305                                checks.add(check);
306                            } else if (exception == null) {
307                                exception = check.getException();
308                            }
309                        }
310                    }
311
312                    if (!checks.isEmpty()) {
313                        RepositoryPolicy policy = getPolicy(session, repository, metadata.getNature());
314
315                        // install path may be different from lookup path
316                        File installFile = new File(
317                                session.getLocalRepository().getBasedir(),
318                                session.getLocalRepositoryManager()
319                                        .getPathForRemoteMetadata(
320                                                metadata, request.getRepository(), request.getRequestContext()));
321
322                        ResolveTask task = new ResolveTask(
323                                session, trace, result, installFile, checks, policy.getChecksumPolicy());
324                        tasks.add(task);
325                    } else {
326                        result.setException(exception);
327                        if (metadataFile != null) {
328                            metadata = metadata.setFile(metadataFile);
329                            result.setMetadata(metadata);
330                        }
331                        metadataResolved(session, trace, metadata, repository, result.getException());
332                    }
333                }
334
335                if (!tasks.isEmpty() && current == shared) {
336                    current.close();
337                    current = exclusive;
338                    continue;
339                }
340
341                if (!tasks.isEmpty()) {
342                    int threads = ExecutorUtils.threadCount(session, 4, CONFIG_PROP_THREADS);
343                    Executor executor = ExecutorUtils.executor(
344                            Math.min(tasks.size(), threads), getClass().getSimpleName() + '-');
345                    try {
346                        RunnableErrorForwarder errorForwarder = new RunnableErrorForwarder();
347
348                        for (ResolveTask task : tasks) {
349                            metadataDownloading(
350                                    task.session, task.trace, task.request.getMetadata(), task.request.getRepository());
351
352                            executor.execute(errorForwarder.wrap(task));
353                        }
354
355                        errorForwarder.await();
356
357                        for (ResolveTask task : tasks) {
358                            /*
359                             * NOTE: Touch after registration with local repo to ensure concurrent resolution is not
360                             * rejected with "already updated" via session data when actual update to local repo is
361                             * still pending.
362                             */
363                            for (UpdateCheck<Metadata, MetadataTransferException> check : task.checks) {
364                                updateCheckManager.touchMetadata(task.session, check.setException(task.exception));
365                            }
366
367                            metadataDownloaded(
368                                    session,
369                                    task.trace,
370                                    task.request.getMetadata(),
371                                    task.request.getRepository(),
372                                    task.metadataFile,
373                                    task.exception);
374
375                            task.result.setException(task.exception);
376                        }
377                    } finally {
378                        ExecutorUtils.shutdown(executor);
379                    }
380                    for (ResolveTask task : tasks) {
381                        Metadata metadata = task.request.getMetadata();
382                        // re-lookup metadata for resolve
383                        LocalMetadataRequest localRequest = new LocalMetadataRequest(
384                                metadata, task.request.getRepository(), task.request.getRequestContext());
385                        File metadataFile = session.getLocalRepositoryManager()
386                                .find(session, localRequest)
387                                .getFile();
388                        if (metadataFile != null) {
389                            metadata = metadata.setFile(metadataFile);
390                            task.result.setMetadata(metadata);
391                        }
392                        if (task.result.getException() == null) {
393                            task.result.setUpdated(true);
394                        }
395                        metadataResolved(
396                                session,
397                                task.trace,
398                                metadata,
399                                task.request.getRepository(),
400                                task.result.getException());
401                    }
402                }
403
404                return results;
405            }
406        } finally {
407            current.close();
408        }
409    }
410
411    private File getLocalFile(RepositorySystemSession session, Metadata metadata) {
412        LocalRepositoryManager lrm = session.getLocalRepositoryManager();
413        LocalMetadataResult localResult = lrm.find(session, new LocalMetadataRequest(metadata, null, null));
414        return localResult.getFile();
415    }
416
417    private List<RemoteRepository> getEnabledSourceRepositories(RemoteRepository repository, Metadata.Nature nature) {
418        List<RemoteRepository> repositories = new ArrayList<>();
419
420        if (repository.isRepositoryManager()) {
421            for (RemoteRepository repo : repository.getMirroredRepositories()) {
422                if (isEnabled(repo, nature)) {
423                    repositories.add(repo);
424                }
425            }
426        } else if (isEnabled(repository, nature)) {
427            repositories.add(repository);
428        }
429
430        return repositories;
431    }
432
433    private boolean isEnabled(RemoteRepository repository, Metadata.Nature nature) {
434        if (!Metadata.Nature.SNAPSHOT.equals(nature)
435                && repository.getPolicy(false).isEnabled()) {
436            return true;
437        }
438        return !Metadata.Nature.RELEASE.equals(nature)
439                && repository.getPolicy(true).isEnabled();
440    }
441
442    private RepositoryPolicy getPolicy(
443            RepositorySystemSession session, RemoteRepository repository, Metadata.Nature nature) {
444        boolean releases = !Metadata.Nature.SNAPSHOT.equals(nature);
445        boolean snapshots = !Metadata.Nature.RELEASE.equals(nature);
446        return remoteRepositoryManager.getPolicy(session, repository, releases, snapshots);
447    }
448
449    private void metadataResolving(
450            RepositorySystemSession session, RequestTrace trace, Metadata metadata, ArtifactRepository repository) {
451        RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_RESOLVING);
452        event.setTrace(trace);
453        event.setMetadata(metadata);
454        event.setRepository(repository);
455
456        repositoryEventDispatcher.dispatch(event.build());
457    }
458
459    private void metadataResolved(
460            RepositorySystemSession session,
461            RequestTrace trace,
462            Metadata metadata,
463            ArtifactRepository repository,
464            Exception exception) {
465        RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_RESOLVED);
466        event.setTrace(trace);
467        event.setMetadata(metadata);
468        event.setRepository(repository);
469        event.setException(exception);
470        event.setFile(metadata.getFile());
471
472        repositoryEventDispatcher.dispatch(event.build());
473    }
474
475    private void metadataDownloading(
476            RepositorySystemSession session, RequestTrace trace, Metadata metadata, ArtifactRepository repository) {
477        RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_DOWNLOADING);
478        event.setTrace(trace);
479        event.setMetadata(metadata);
480        event.setRepository(repository);
481
482        repositoryEventDispatcher.dispatch(event.build());
483    }
484
485    private void metadataDownloaded(
486            RepositorySystemSession session,
487            RequestTrace trace,
488            Metadata metadata,
489            ArtifactRepository repository,
490            File file,
491            Exception exception) {
492        RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_DOWNLOADED);
493        event.setTrace(trace);
494        event.setMetadata(metadata);
495        event.setRepository(repository);
496        event.setException(exception);
497        event.setFile(file);
498
499        repositoryEventDispatcher.dispatch(event.build());
500    }
501
502    class ResolveTask implements Runnable {
503        final RepositorySystemSession session;
504
505        final RequestTrace trace;
506
507        final MetadataResult result;
508
509        final MetadataRequest request;
510
511        final File metadataFile;
512
513        final String policy;
514
515        final List<UpdateCheck<Metadata, MetadataTransferException>> checks;
516
517        volatile MetadataTransferException exception;
518
519        ResolveTask(
520                RepositorySystemSession session,
521                RequestTrace trace,
522                MetadataResult result,
523                File metadataFile,
524                List<UpdateCheck<Metadata, MetadataTransferException>> checks,
525                String policy) {
526            this.session = session;
527            this.trace = trace;
528            this.result = result;
529            this.request = result.getRequest();
530            this.metadataFile = metadataFile;
531            this.policy = policy;
532            this.checks = checks;
533        }
534
535        public void run() {
536            Metadata metadata = request.getMetadata();
537            RemoteRepository requestRepository = request.getRepository();
538
539            try {
540                List<RemoteRepository> repositories = new ArrayList<>();
541                for (UpdateCheck<Metadata, MetadataTransferException> check : checks) {
542                    repositories.add(check.getAuthoritativeRepository());
543                }
544
545                MetadataDownload download = new MetadataDownload();
546                download.setMetadata(metadata);
547                download.setRequestContext(request.getRequestContext());
548                download.setFile(metadataFile);
549                download.setChecksumPolicy(policy);
550                download.setRepositories(repositories);
551                download.setListener(SafeTransferListener.wrap(session));
552                download.setTrace(trace);
553
554                try (RepositoryConnector connector =
555                        repositoryConnectorProvider.newRepositoryConnector(session, requestRepository)) {
556                    connector.get(null, Collections.singletonList(download));
557                }
558
559                exception = download.getException();
560
561                if (exception == null) {
562
563                    List<String> contexts = Collections.singletonList(request.getRequestContext());
564                    LocalMetadataRegistration registration =
565                            new LocalMetadataRegistration(metadata, requestRepository, contexts);
566
567                    session.getLocalRepositoryManager().add(session, registration);
568                } else if (request.isDeleteLocalCopyIfMissing() && exception instanceof MetadataNotFoundException) {
569                    download.getFile().delete();
570                }
571            } catch (NoRepositoryConnectorException e) {
572                exception = new MetadataTransferException(metadata, requestRepository, e);
573            }
574        }
575    }
576}