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