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