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