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