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