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