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