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