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.Collections;
024import java.util.HashMap;
025import java.util.Map;
026import static java.util.Objects.requireNonNull;
027import java.util.Properties;
028import java.util.Set;
029import java.util.TreeSet;
030import java.util.concurrent.ConcurrentHashMap;
031
032import javax.inject.Inject;
033import javax.inject.Named;
034import javax.inject.Singleton;
035
036import org.eclipse.aether.RepositorySystemSession;
037import org.eclipse.aether.SessionData;
038import org.eclipse.aether.artifact.Artifact;
039import org.eclipse.aether.impl.UpdateCheck;
040import org.eclipse.aether.impl.UpdateCheckManager;
041import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
042import org.eclipse.aether.metadata.Metadata;
043import org.eclipse.aether.repository.AuthenticationDigest;
044import org.eclipse.aether.repository.Proxy;
045import org.eclipse.aether.repository.RemoteRepository;
046import org.eclipse.aether.resolution.ResolutionErrorPolicy;
047import org.eclipse.aether.spi.locator.Service;
048import org.eclipse.aether.spi.locator.ServiceLocator;
049import org.eclipse.aether.transfer.ArtifactNotFoundException;
050import org.eclipse.aether.transfer.ArtifactTransferException;
051import org.eclipse.aether.transfer.MetadataNotFoundException;
052import org.eclipse.aether.transfer.MetadataTransferException;
053import org.eclipse.aether.util.ConfigUtils;
054import org.slf4j.Logger;
055import org.slf4j.LoggerFactory;
056
057/**
058 */
059@Singleton
060@Named
061public class DefaultUpdateCheckManager
062    implements UpdateCheckManager, Service
063{
064
065    private static final Logger LOGGER = LoggerFactory.getLogger( DefaultUpdatePolicyAnalyzer.class );
066
067    private TrackingFileManager trackingFileManager;
068
069    private UpdatePolicyAnalyzer updatePolicyAnalyzer;
070
071    private static final String UPDATED_KEY_SUFFIX = ".lastUpdated";
072
073    private static final String ERROR_KEY_SUFFIX = ".error";
074
075    private static final String NOT_FOUND = "";
076
077    private static final String SESSION_CHECKS = "updateCheckManager.checks";
078
079    static final String CONFIG_PROP_SESSION_STATE = "aether.updateCheckManager.sessionState";
080
081    private static final int STATE_ENABLED = 0;
082
083    private static final int STATE_BYPASS = 1;
084
085    private static final int STATE_DISABLED = 2;
086
087    public DefaultUpdateCheckManager()
088    {
089        // default ctor for ServiceLocator
090    }
091
092    @Inject
093    DefaultUpdateCheckManager( TrackingFileManager trackingFileManager, UpdatePolicyAnalyzer updatePolicyAnalyzer )
094    {
095        setTrackingFileManager( trackingFileManager );
096        setUpdatePolicyAnalyzer( updatePolicyAnalyzer );
097    }
098
099    public void initService( ServiceLocator locator )
100    {
101        setTrackingFileManager( locator.getService( TrackingFileManager.class ) );
102        setUpdatePolicyAnalyzer( locator.getService( UpdatePolicyAnalyzer.class ) );
103    }
104
105    public DefaultUpdateCheckManager setTrackingFileManager( TrackingFileManager trackingFileManager )
106    {
107        this.trackingFileManager = requireNonNull( trackingFileManager );
108        return this;
109    }
110
111    public DefaultUpdateCheckManager setUpdatePolicyAnalyzer( UpdatePolicyAnalyzer updatePolicyAnalyzer )
112    {
113        this.updatePolicyAnalyzer = requireNonNull( updatePolicyAnalyzer, "update policy analyzer cannot be null" );
114        return this;
115    }
116
117    public void checkArtifact( RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check )
118    {
119        if ( check.getLocalLastUpdated() != 0
120            && !isUpdatedRequired( session, check.getLocalLastUpdated(), check.getPolicy() ) )
121        {
122            LOGGER.debug( "Skipped remote request for {}, locally installed artifact up-to-date", check.getItem() );
123
124            check.setRequired( false );
125            return;
126        }
127
128        Artifact artifact = check.getItem();
129        RemoteRepository repository = check.getRepository();
130
131        File artifactFile = requireNonNull( check.getFile(), String.format( "The artifact '%s' has no file attached",
132                artifact ) );
133
134        boolean fileExists = check.isFileValid() && artifactFile.exists();
135
136        File touchFile = getArtifactTouchFile( artifactFile );
137        Properties props = read( touchFile );
138
139        String updateKey = getUpdateKey( session, artifactFile, repository );
140        String dataKey = getDataKey( repository );
141
142        String error = getError( props, dataKey );
143
144        long lastUpdated;
145        if ( error == null )
146        {
147            if ( fileExists )
148            {
149                // last update was successful
150                lastUpdated = artifactFile.lastModified();
151            }
152            else
153            {
154                // this is the first attempt ever
155                lastUpdated = 0L;
156            }
157        }
158        else if ( error.length() <= 0 )
159        {
160            // artifact did not exist
161            lastUpdated = getLastUpdated( props, dataKey );
162        }
163        else
164        {
165            // artifact could not be transferred
166            String transferKey = getTransferKey( session, repository );
167            lastUpdated = getLastUpdated( props, transferKey );
168        }
169
170        if ( lastUpdated == 0L )
171        {
172            check.setRequired( true );
173        }
174        else if ( isAlreadyUpdated( session, updateKey ) )
175        {
176            LOGGER.debug( "Skipped remote request for {}, already updated during this session", check.getItem() );
177
178            check.setRequired( false );
179            if ( error != null )
180            {
181                check.setException( newException( error, artifact, repository ) );
182            }
183        }
184        else if ( isUpdatedRequired( session, lastUpdated, check.getPolicy() ) )
185        {
186            check.setRequired( true );
187        }
188        else if ( fileExists )
189        {
190            LOGGER.debug( "Skipped remote request for {}, locally cached artifact up-to-date", check.getItem() );
191
192            check.setRequired( false );
193        }
194        else
195        {
196            int errorPolicy = Utils.getPolicy( session, artifact, repository );
197            int cacheFlag = getCacheFlag( error );
198            if ( ( errorPolicy & cacheFlag ) != 0 )
199            {
200                check.setRequired( false );
201                check.setException( newException( error, artifact, repository ) );
202            }
203            else
204            {
205                check.setRequired( true );
206            }
207        }
208    }
209
210    private static int getCacheFlag( String error )
211    {
212        if ( error == null || error.length() <= 0 )
213        {
214            return ResolutionErrorPolicy.CACHE_NOT_FOUND;
215        }
216        else
217        {
218            return ResolutionErrorPolicy.CACHE_TRANSFER_ERROR;
219        }
220    }
221
222    private ArtifactTransferException newException( String error, Artifact artifact, RemoteRepository repository )
223    {
224        if ( error == null || error.length() <= 0 )
225        {
226            return new ArtifactNotFoundException( artifact, repository, artifact
227                + " was not found in " + repository.getUrl() + " during a previous attempt. This failure was"
228                + " cached in the local repository and"
229                + " resolution is not reattempted until the update interval of " + repository.getId()
230                + " has elapsed or updates are forced", true );
231        }
232        else
233        {
234            return new ArtifactTransferException( artifact, repository, artifact + " failed to transfer from "
235                + repository.getUrl() + " during a previous attempt. This failure"
236                + " was cached in the local repository and"
237                + " resolution is not reattempted until the update interval of " + repository.getId()
238                + " has elapsed or updates are forced. Original error: " + error, true );
239        }
240    }
241
242    public void checkMetadata( RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check )
243    {
244        if ( check.getLocalLastUpdated() != 0
245            && !isUpdatedRequired( session, check.getLocalLastUpdated(), check.getPolicy() ) )
246        {
247            LOGGER.debug( "Skipped remote request for {} locally installed metadata up-to-date", check.getItem() );
248
249            check.setRequired( false );
250            return;
251        }
252
253        Metadata metadata = check.getItem();
254        RemoteRepository repository = check.getRepository();
255
256        File metadataFile = requireNonNull( check.getFile(), String.format( "The metadata '%s' has no file attached",
257                metadata ) );
258
259        boolean fileExists = check.isFileValid() && metadataFile.exists();
260
261        File touchFile = getMetadataTouchFile( metadataFile );
262        Properties props = read( touchFile );
263
264        String updateKey = getUpdateKey( session, metadataFile, repository );
265        String dataKey = getDataKey( metadataFile );
266
267        String error = getError( props, dataKey );
268
269        long lastUpdated;
270        if ( error == null )
271        {
272            if ( fileExists )
273            {
274                // last update was successful
275                lastUpdated = getLastUpdated( props, dataKey );
276            }
277            else
278            {
279                // this is the first attempt ever
280                lastUpdated = 0L;
281            }
282        }
283        else if ( error.length() <= 0 )
284        {
285            // metadata did not exist
286            lastUpdated = getLastUpdated( props, dataKey );
287        }
288        else
289        {
290            // metadata could not be transferred
291            String transferKey = getTransferKey( session, metadataFile, repository );
292            lastUpdated = getLastUpdated( props, transferKey );
293        }
294
295        if ( lastUpdated == 0L )
296        {
297            check.setRequired( true );
298        }
299        else if ( isAlreadyUpdated( session, updateKey ) )
300        {
301            LOGGER.debug( "Skipped remote request for {}, already updated during this session", check.getItem() );
302
303            check.setRequired( false );
304            if ( error != null )
305            {
306                check.setException( newException( error, metadata, repository ) );
307            }
308        }
309        else if ( isUpdatedRequired( session, lastUpdated, check.getPolicy() ) )
310        {
311            check.setRequired( true );
312        }
313        else if ( fileExists )
314        {
315            LOGGER.debug( "Skipped remote request for {}, locally cached metadata up-to-date", check.getItem() );
316
317            check.setRequired( false );
318        }
319        else
320        {
321            int errorPolicy = Utils.getPolicy( session, metadata, repository );
322            int cacheFlag = getCacheFlag( error );
323            if ( ( errorPolicy & cacheFlag ) != 0 )
324            {
325                check.setRequired( false );
326                check.setException( newException( error, metadata, repository ) );
327            }
328            else
329            {
330                check.setRequired( true );
331            }
332        }
333    }
334
335    private MetadataTransferException newException( String error, Metadata metadata, RemoteRepository repository )
336    {
337        if ( error == null || error.length() <= 0 )
338        {
339            return new MetadataNotFoundException( metadata, repository, metadata + " was not found in "
340                + repository.getUrl() + " during a previous attempt."
341                + " This failure was cached in the local repository and"
342                + " resolution is not be reattempted until the update interval of " + repository.getId()
343                + " has elapsed or updates are forced", true );
344        }
345        else
346        {
347            return new MetadataTransferException( metadata, repository, metadata + " failed to transfer from "
348                + repository.getUrl() + " during a previous attempt."
349                + " This failure was cached in the local repository and"
350                + " resolution will not be reattempted until the update interval of " + repository.getId()
351                + " has elapsed or updates are forced. Original error: " + error, true );
352        }
353    }
354
355    private long getLastUpdated( Properties props, String key )
356    {
357        String value = props.getProperty( key + UPDATED_KEY_SUFFIX, "" );
358        try
359        {
360            return ( value.length() > 0 ) ? Long.parseLong( value ) : 1;
361        }
362        catch ( NumberFormatException e )
363        {
364            LOGGER.debug( "Cannot parse last updated date {}, ignoring it", value, e );
365            return 1;
366        }
367    }
368
369    private String getError( Properties props, String key )
370    {
371        return props.getProperty( key + ERROR_KEY_SUFFIX );
372    }
373
374    private File getArtifactTouchFile( File artifactFile )
375    {
376        return new File( artifactFile.getPath() + UPDATED_KEY_SUFFIX );
377    }
378
379    private File getMetadataTouchFile( File metadataFile )
380    {
381        return new File( metadataFile.getParent(), "resolver-status.properties" );
382    }
383
384    private String getDataKey( RemoteRepository repository )
385    {
386        Set<String> mirroredUrls = Collections.emptySet();
387        if ( repository.isRepositoryManager() )
388        {
389            mirroredUrls = new TreeSet<>();
390            for ( RemoteRepository mirroredRepository : repository.getMirroredRepositories() )
391            {
392                mirroredUrls.add( normalizeRepoUrl( mirroredRepository.getUrl() ) );
393            }
394        }
395
396        StringBuilder buffer = new StringBuilder( 1024 );
397
398        buffer.append( normalizeRepoUrl( repository.getUrl() ) );
399        for ( String mirroredUrl : mirroredUrls )
400        {
401            buffer.append( '+' ).append( mirroredUrl );
402        }
403
404        return buffer.toString();
405    }
406
407    private String getTransferKey( RepositorySystemSession session, RemoteRepository repository )
408    {
409        return getRepoKey( session, repository );
410    }
411
412    private String getDataKey( File metadataFile )
413    {
414        return metadataFile.getName();
415    }
416
417    private String getTransferKey( RepositorySystemSession session, File metadataFile,
418                                  RemoteRepository repository )
419    {
420        return metadataFile.getName() + '/' + getRepoKey( session, repository );
421    }
422
423    private String getRepoKey( RepositorySystemSession session, RemoteRepository repository )
424    {
425        StringBuilder buffer = new StringBuilder( 128 );
426
427        Proxy proxy = repository.getProxy();
428        if ( proxy != null )
429        {
430            buffer.append( AuthenticationDigest.forProxy( session, repository ) ).append( '@' );
431            buffer.append( proxy.getHost() ).append( ':' ).append( proxy.getPort() ).append( '>' );
432        }
433
434        buffer.append( AuthenticationDigest.forRepository( session, repository ) ).append( '@' );
435
436        buffer.append( repository.getContentType() ).append( '-' );
437        buffer.append( repository.getId() ).append( '-' );
438        buffer.append( normalizeRepoUrl( repository.getUrl() ) );
439
440        return buffer.toString();
441    }
442
443    private String normalizeRepoUrl( String url )
444    {
445        String result = url;
446        if ( url != null && url.length() > 0 && !url.endsWith( "/" ) )
447        {
448            result = url + '/';
449        }
450        return result;
451    }
452
453    private String getUpdateKey( RepositorySystemSession session, File file, RemoteRepository repository )
454    {
455        return file.getAbsolutePath() + '|' + getRepoKey( session, repository );
456    }
457
458    private int getSessionState( RepositorySystemSession session )
459    {
460        String mode = ConfigUtils.getString( session, "enabled", CONFIG_PROP_SESSION_STATE );
461        if ( Boolean.parseBoolean( mode ) || "enabled".equalsIgnoreCase( mode ) )
462        {
463            // perform update check at most once per session, regardless of update policy
464            return STATE_ENABLED;
465        }
466        else if ( "bypass".equalsIgnoreCase( mode ) )
467        {
468            // evaluate update policy but record update in session to prevent potential future checks
469            return STATE_BYPASS;
470        }
471        else
472        {
473            // no session state at all, always evaluate update policy
474            return STATE_DISABLED;
475        }
476    }
477
478    private boolean isAlreadyUpdated( RepositorySystemSession session, Object updateKey )
479    {
480        if ( getSessionState( session ) >= STATE_BYPASS )
481        {
482            return false;
483        }
484        SessionData data = session.getData();
485        Object checkedFiles = data.get( SESSION_CHECKS );
486        if ( !( checkedFiles instanceof Map ) )
487        {
488            return false;
489        }
490        return ( (Map<?, ?>) checkedFiles ).containsKey( updateKey );
491    }
492
493    @SuppressWarnings( "unchecked" )
494    private void setUpdated( RepositorySystemSession session, Object updateKey )
495    {
496        if ( getSessionState( session ) >= STATE_DISABLED )
497        {
498            return;
499        }
500        SessionData data = session.getData();
501        Object checkedFiles = data.get( SESSION_CHECKS );
502        while ( !( checkedFiles instanceof Map ) )
503        {
504            Object old = checkedFiles;
505            checkedFiles = new ConcurrentHashMap<>( 256 );
506            if ( data.set( SESSION_CHECKS, old, checkedFiles ) )
507            {
508                break;
509            }
510            checkedFiles = data.get( SESSION_CHECKS );
511        }
512        ( (Map<Object, Boolean>) checkedFiles ).put( updateKey, Boolean.TRUE );
513    }
514
515    private boolean isUpdatedRequired( RepositorySystemSession session, long lastModified, String policy )
516    {
517        return updatePolicyAnalyzer.isUpdatedRequired( session, lastModified, policy );
518    }
519
520    private Properties read( File touchFile )
521    {
522        Properties props = trackingFileManager.read( touchFile );
523        return ( props != null ) ? props : new Properties();
524    }
525
526    public void touchArtifact( RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check )
527    {
528        File artifactFile = check.getFile();
529        File touchFile = getArtifactTouchFile( artifactFile );
530
531        String updateKey = getUpdateKey( session, artifactFile, check.getRepository() );
532        String dataKey = getDataKey( check.getAuthoritativeRepository() );
533        String transferKey = getTransferKey( session, check.getRepository() );
534
535        setUpdated( session, updateKey );
536        Properties props = write( touchFile, dataKey, transferKey, check.getException() );
537
538        if ( artifactFile.exists() && !hasErrors( props ) )
539        {
540            touchFile.delete();
541        }
542    }
543
544    private boolean hasErrors( Properties props )
545    {
546        for ( Object key : props.keySet() )
547        {
548            if ( key.toString().endsWith( ERROR_KEY_SUFFIX ) )
549            {
550                return true;
551            }
552        }
553        return false;
554    }
555
556    public void touchMetadata( RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check )
557    {
558        File metadataFile = check.getFile();
559        File touchFile = getMetadataTouchFile( metadataFile );
560
561        String updateKey = getUpdateKey( session, metadataFile, check.getRepository() );
562        String dataKey = getDataKey( metadataFile );
563        String transferKey = getTransferKey( session, metadataFile, check.getRepository() );
564
565        setUpdated( session, updateKey );
566        write( touchFile, dataKey, transferKey, check.getException() );
567    }
568
569    private Properties write( File touchFile, String dataKey, String transferKey, Exception error )
570    {
571        Map<String, String> updates = new HashMap<>();
572
573        String timestamp = Long.toString( System.currentTimeMillis() );
574
575        if ( error == null )
576        {
577            updates.put( dataKey + ERROR_KEY_SUFFIX, null );
578            updates.put( dataKey + UPDATED_KEY_SUFFIX, timestamp );
579            updates.put( transferKey + UPDATED_KEY_SUFFIX, null );
580        }
581        else if ( error instanceof ArtifactNotFoundException || error instanceof MetadataNotFoundException )
582        {
583            updates.put( dataKey + ERROR_KEY_SUFFIX, NOT_FOUND );
584            updates.put( dataKey + UPDATED_KEY_SUFFIX, timestamp );
585            updates.put( transferKey + UPDATED_KEY_SUFFIX, null );
586        }
587        else
588        {
589            String msg = error.getMessage();
590            if ( msg == null || msg.length() <= 0 )
591            {
592                msg = error.getClass().getSimpleName();
593            }
594            updates.put( dataKey + ERROR_KEY_SUFFIX, msg );
595            updates.put( dataKey + UPDATED_KEY_SUFFIX, null );
596            updates.put( transferKey + UPDATED_KEY_SUFFIX, timestamp );
597        }
598
599        return trackingFileManager.update( touchFile, updates );
600    }
601
602}