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