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            if ( LOGGER.isDebugEnabled() )
165            {
166                LOGGER.debug( "Skipped remote request for " + check.getItem()
167                    + ", already updated during this session." );
168            }
169
170            check.setRequired( false );
171            if ( error != null )
172            {
173                check.setException( newException( error, artifact, repository ) );
174            }
175        }
176        else if ( isUpdatedRequired( session, lastUpdated, check.getPolicy() ) )
177        {
178            check.setRequired( true );
179        }
180        else if ( fileExists )
181        {
182            LOGGER.debug( "Skipped remote request for {}, locally cached artifact up-to-date.", check.getItem() );
183
184            check.setRequired( false );
185        }
186        else
187        {
188            int errorPolicy = Utils.getPolicy( session, artifact, repository );
189            int cacheFlag = getCacheFlag( error );
190            if ( ( errorPolicy & cacheFlag ) != 0 )
191            {
192                check.setRequired( false );
193                check.setException( newException( error, artifact, repository ) );
194            }
195            else
196            {
197                check.setRequired( true );
198            }
199        }
200    }
201
202    private static int getCacheFlag( String error )
203    {
204        if ( error == null || error.length() <= 0 )
205        {
206            return ResolutionErrorPolicy.CACHE_NOT_FOUND;
207        }
208        else
209        {
210            return ResolutionErrorPolicy.CACHE_TRANSFER_ERROR;
211        }
212    }
213
214    private ArtifactTransferException newException( String error, Artifact artifact, RemoteRepository repository )
215    {
216        if ( error == null || error.length() <= 0 )
217        {
218            return new ArtifactNotFoundException( artifact, repository, "Failure to find " + artifact + " in "
219                + repository.getUrl() + " was cached in the local repository, "
220                + "resolution will not be reattempted until the update interval of " + repository.getId()
221                + " has elapsed or updates are forced", true );
222        }
223        else
224        {
225            return new ArtifactTransferException( artifact, repository, "Failure to transfer " + artifact + " from "
226                + repository.getUrl() + " was cached in the local repository, "
227                + "resolution will not be reattempted until the update interval of " + repository.getId()
228                + " has elapsed or updates are forced. Original error: " + error, true );
229        }
230    }
231
232    public void checkMetadata( RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check )
233    {
234        if ( check.getLocalLastUpdated() != 0
235            && !isUpdatedRequired( session, check.getLocalLastUpdated(), check.getPolicy() ) )
236        {
237            LOGGER.debug( "Skipped remote request for {} locally installed metadata up-to-date.", check.getItem() );
238
239            check.setRequired( false );
240            return;
241        }
242
243        Metadata metadata = check.getItem();
244        RemoteRepository repository = check.getRepository();
245
246        File metadataFile = requireNonNull( check.getFile(), String.format( "The metadata '%s' has no file attached",
247                metadata ) );
248
249        boolean fileExists = check.isFileValid() && metadataFile.exists();
250
251        File touchFile = getTouchFile( metadata, metadataFile );
252        Properties props = read( touchFile );
253
254        String updateKey = getUpdateKey( session, metadataFile, repository );
255        String dataKey = getDataKey( metadata, metadataFile, check.getAuthoritativeRepository() );
256
257        String error = getError( props, dataKey );
258
259        long lastUpdated;
260        if ( error == null )
261        {
262            if ( fileExists )
263            {
264                // last update was successful
265                lastUpdated = getLastUpdated( props, dataKey );
266            }
267            else
268            {
269                // this is the first attempt ever
270                lastUpdated = 0L;
271            }
272        }
273        else if ( error.length() <= 0 )
274        {
275            // metadata did not exist
276            lastUpdated = getLastUpdated( props, dataKey );
277        }
278        else
279        {
280            // metadata could not be transferred
281            String transferKey = getTransferKey( session, metadata, metadataFile, repository );
282            lastUpdated = getLastUpdated( props, transferKey );
283        }
284
285        if ( lastUpdated == 0L )
286        {
287            check.setRequired( true );
288        }
289        else if ( isAlreadyUpdated( session, updateKey ) )
290        {
291            LOGGER.debug( "Skipped remote request for {}, already updated during this session.", check.getItem() );
292
293            check.setRequired( false );
294            if ( error != null )
295            {
296                check.setException( newException( error, metadata, repository ) );
297            }
298        }
299        else if ( isUpdatedRequired( session, lastUpdated, check.getPolicy() ) )
300        {
301            check.setRequired( true );
302        }
303        else if ( fileExists )
304        {
305            LOGGER.debug( "Skipped remote request for {}, locally cached metadata up-to-date.", check.getItem() );
306
307            check.setRequired( false );
308        }
309        else
310        {
311            int errorPolicy = Utils.getPolicy( session, metadata, repository );
312            int cacheFlag = getCacheFlag( error );
313            if ( ( errorPolicy & cacheFlag ) != 0 )
314            {
315                check.setRequired( false );
316                check.setException( newException( error, metadata, repository ) );
317            }
318            else
319            {
320                check.setRequired( true );
321            }
322        }
323    }
324
325    private MetadataTransferException newException( String error, Metadata metadata, RemoteRepository repository )
326    {
327        if ( error == null || error.length() <= 0 )
328        {
329            return new MetadataNotFoundException( metadata, repository, "Failure to find " + metadata + " in "
330                + repository.getUrl() + " was cached in the local repository, "
331                + "resolution will not be reattempted until the update interval of " + repository.getId()
332                + " has elapsed or updates are forced", true );
333        }
334        else
335        {
336            return new MetadataTransferException( metadata, repository, "Failure to transfer " + metadata + " from "
337                + repository.getUrl() + " was cached in the local repository, "
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 lastUpdated date: \'{}\'. Ignoring.", 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, "true", CONFIG_PROP_SESSION_STATE );
450        if ( Boolean.parseBoolean( 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}