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