View Javadoc
1   package org.eclipse.aether.internal.impl;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.Map;
26  import static java.util.Objects.requireNonNull;
27  import java.util.Properties;
28  import java.util.Set;
29  import java.util.TreeSet;
30  import java.util.concurrent.ConcurrentHashMap;
31  
32  import javax.inject.Inject;
33  import javax.inject.Named;
34  import javax.inject.Singleton;
35  
36  import org.eclipse.aether.RepositorySystemSession;
37  import org.eclipse.aether.SessionData;
38  import org.eclipse.aether.artifact.Artifact;
39  import org.eclipse.aether.impl.UpdateCheck;
40  import org.eclipse.aether.impl.UpdateCheckManager;
41  import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
42  import org.eclipse.aether.metadata.Metadata;
43  import org.eclipse.aether.repository.AuthenticationDigest;
44  import org.eclipse.aether.repository.Proxy;
45  import org.eclipse.aether.repository.RemoteRepository;
46  import org.eclipse.aether.resolution.ResolutionErrorPolicy;
47  import org.eclipse.aether.spi.locator.Service;
48  import org.eclipse.aether.spi.locator.ServiceLocator;
49  import org.eclipse.aether.transfer.ArtifactNotFoundException;
50  import org.eclipse.aether.transfer.ArtifactTransferException;
51  import org.eclipse.aether.transfer.MetadataNotFoundException;
52  import org.eclipse.aether.transfer.MetadataTransferException;
53  import org.eclipse.aether.util.ConfigUtils;
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  
57  /**
58   */
59  @Singleton
60  @Named
61  public class DefaultUpdateCheckManager
62      implements UpdateCheckManager, Service
63  {
64  
65      private static final Logger LOGGER = LoggerFactory.getLogger( DefaultUpdatePolicyAnalyzer.class );
66  
67      private TrackingFileManager trackingFileManager;
68  
69      private UpdatePolicyAnalyzer updatePolicyAnalyzer;
70  
71      private static final String UPDATED_KEY_SUFFIX = ".lastUpdated";
72  
73      private static final String ERROR_KEY_SUFFIX = ".error";
74  
75      private static final String NOT_FOUND = "";
76  
77      private static final String SESSION_CHECKS = "updateCheckManager.checks";
78  
79      static final String CONFIG_PROP_SESSION_STATE = "aether.updateCheckManager.sessionState";
80  
81      private static final int STATE_ENABLED = 0;
82  
83      private static final int STATE_BYPASS = 1;
84  
85      private static final int STATE_DISABLED = 2;
86  
87      public DefaultUpdateCheckManager()
88      {
89          // default ctor for ServiceLocator
90      }
91  
92      @Inject
93      DefaultUpdateCheckManager( TrackingFileManager trackingFileManager, UpdatePolicyAnalyzer updatePolicyAnalyzer )
94      {
95          setTrackingFileManager( trackingFileManager );
96          setUpdatePolicyAnalyzer( updatePolicyAnalyzer );
97      }
98  
99      public void initService( ServiceLocator locator )
100     {
101         setTrackingFileManager( locator.getService( TrackingFileManager.class ) );
102         setUpdatePolicyAnalyzer( locator.getService( UpdatePolicyAnalyzer.class ) );
103     }
104 
105     public DefaultUpdateCheckManager setTrackingFileManager( TrackingFileManager trackingFileManager )
106     {
107         this.trackingFileManager = requireNonNull( trackingFileManager );
108         return this;
109     }
110 
111     public DefaultUpdateCheckManager setUpdatePolicyAnalyzer( UpdatePolicyAnalyzer updatePolicyAnalyzer )
112     {
113         this.updatePolicyAnalyzer = requireNonNull( updatePolicyAnalyzer, "update policy analyzer cannot be null" );
114         return this;
115     }
116 
117     public void checkArtifact( RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check )
118     {
119         requireNonNull( session, "session cannot be null" );
120         requireNonNull( check, "check cannot be null" );
121         if ( check.getLocalLastUpdated() != 0
122             && !isUpdatedRequired( session, check.getLocalLastUpdated(), check.getPolicy() ) )
123         {
124             LOGGER.debug( "Skipped remote request for {}, locally installed artifact up-to-date", check.getItem() );
125 
126             check.setRequired( false );
127             return;
128         }
129 
130         Artifact artifact = check.getItem();
131         RemoteRepository repository = check.getRepository();
132 
133         File artifactFile = requireNonNull( check.getFile(), String.format( "The artifact '%s' has no file attached",
134                 artifact ) );
135 
136         boolean fileExists = check.isFileValid() && artifactFile.exists();
137 
138         File touchFile = getArtifactTouchFile( artifactFile );
139         Properties props = read( touchFile );
140 
141         String updateKey = getUpdateKey( session, artifactFile, repository );
142         String dataKey = getDataKey( repository );
143 
144         String error = getError( props, dataKey );
145 
146         long lastUpdated;
147         if ( error == null )
148         {
149             if ( fileExists )
150             {
151                 // last update was successful
152                 lastUpdated = artifactFile.lastModified();
153             }
154             else
155             {
156                 // this is the first attempt ever
157                 lastUpdated = 0L;
158             }
159         }
160         else if ( error.isEmpty() )
161         {
162             // artifact did not exist
163             lastUpdated = getLastUpdated( props, dataKey );
164         }
165         else
166         {
167             // artifact could not be transferred
168             String transferKey = getTransferKey( session, repository );
169             lastUpdated = getLastUpdated( props, transferKey );
170         }
171 
172         if ( lastUpdated == 0L )
173         {
174             check.setRequired( true );
175         }
176         else if ( isAlreadyUpdated( session, updateKey ) )
177         {
178             LOGGER.debug( "Skipped remote request for {}, already updated during this session", check.getItem() );
179 
180             check.setRequired( false );
181             if ( error != null )
182             {
183                 check.setException( newException( error, artifact, repository ) );
184             }
185         }
186         else if ( isUpdatedRequired( session, lastUpdated, check.getPolicy() ) )
187         {
188             check.setRequired( true );
189         }
190         else if ( fileExists )
191         {
192             LOGGER.debug( "Skipped remote request for {}, locally cached artifact up-to-date", check.getItem() );
193 
194             check.setRequired( false );
195         }
196         else
197         {
198             int errorPolicy = Utils.getPolicy( session, artifact, repository );
199             int cacheFlag = getCacheFlag( error );
200             if ( ( errorPolicy & cacheFlag ) != 0 )
201             {
202                 check.setRequired( false );
203                 check.setException( newException( error, artifact, repository ) );
204             }
205             else
206             {
207                 check.setRequired( true );
208             }
209         }
210     }
211 
212     private static int getCacheFlag( String error )
213     {
214         if ( error == null || error.isEmpty() )
215         {
216             return ResolutionErrorPolicy.CACHE_NOT_FOUND;
217         }
218         else
219         {
220             return ResolutionErrorPolicy.CACHE_TRANSFER_ERROR;
221         }
222     }
223 
224     private ArtifactTransferException newException( String error, Artifact artifact, RemoteRepository repository )
225     {
226         if ( error == null || error.isEmpty() )
227         {
228             return new ArtifactNotFoundException( artifact, repository, artifact
229                 + " was not found in " + repository.getUrl() + " during a previous attempt. This failure was"
230                 + " cached in the local repository and"
231                 + " resolution is not reattempted until the update interval of " + repository.getId()
232                 + " has elapsed or updates are forced", true );
233         }
234         else
235         {
236             return new ArtifactTransferException( artifact, repository, artifact + " failed to transfer from "
237                 + repository.getUrl() + " during a previous attempt. This failure"
238                 + " was cached in the local repository and"
239                 + " resolution is not reattempted until the update interval of " + repository.getId()
240                 + " has elapsed or updates are forced. Original error: " + error, true );
241         }
242     }
243 
244     public void checkMetadata( RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check )
245     {
246         requireNonNull( session, "session cannot be null" );
247         requireNonNull( check, "check cannot be null" );
248         if ( check.getLocalLastUpdated() != 0
249             && !isUpdatedRequired( session, check.getLocalLastUpdated(), check.getPolicy() ) )
250         {
251             LOGGER.debug( "Skipped remote request for {} locally installed metadata up-to-date", check.getItem() );
252 
253             check.setRequired( false );
254             return;
255         }
256 
257         Metadata metadata = check.getItem();
258         RemoteRepository repository = check.getRepository();
259 
260         File metadataFile = requireNonNull( check.getFile(), String.format( "The metadata '%s' has no file attached",
261                 metadata ) );
262 
263         boolean fileExists = check.isFileValid() && metadataFile.exists();
264 
265         File touchFile = getMetadataTouchFile( metadataFile );
266         Properties props = read( touchFile );
267 
268         String updateKey = getUpdateKey( session, metadataFile, repository );
269         String dataKey = getDataKey( metadataFile );
270 
271         String error = getError( props, dataKey );
272 
273         long lastUpdated;
274         if ( error == null )
275         {
276             if ( fileExists )
277             {
278                 // last update was successful
279                 lastUpdated = getLastUpdated( props, dataKey );
280             }
281             else
282             {
283                 // this is the first attempt ever
284                 lastUpdated = 0L;
285             }
286         }
287         else if ( error.isEmpty() )
288         {
289             // metadata did not exist
290             lastUpdated = getLastUpdated( props, dataKey );
291         }
292         else
293         {
294             // metadata could not be transferred
295             String transferKey = getTransferKey( session, metadataFile, repository );
296             lastUpdated = getLastUpdated( props, transferKey );
297         }
298 
299         if ( lastUpdated == 0L )
300         {
301             check.setRequired( true );
302         }
303         else if ( isAlreadyUpdated( session, updateKey ) )
304         {
305             LOGGER.debug( "Skipped remote request for {}, already updated during this session", check.getItem() );
306 
307             check.setRequired( false );
308             if ( error != null )
309             {
310                 check.setException( newException( error, metadata, repository ) );
311             }
312         }
313         else if ( isUpdatedRequired( session, lastUpdated, check.getPolicy() ) )
314         {
315             check.setRequired( true );
316         }
317         else if ( fileExists )
318         {
319             LOGGER.debug( "Skipped remote request for {}, locally cached metadata up-to-date", check.getItem() );
320 
321             check.setRequired( false );
322         }
323         else
324         {
325             int errorPolicy = Utils.getPolicy( session, metadata, repository );
326             int cacheFlag = getCacheFlag( error );
327             if ( ( errorPolicy & cacheFlag ) != 0 )
328             {
329                 check.setRequired( false );
330                 check.setException( newException( error, metadata, repository ) );
331             }
332             else
333             {
334                 check.setRequired( true );
335             }
336         }
337     }
338 
339     private MetadataTransferException newException( String error, Metadata metadata, RemoteRepository repository )
340     {
341         if ( error == null || error.isEmpty() )
342         {
343             return new MetadataNotFoundException( metadata, repository, metadata + " was not found in "
344                 + repository.getUrl() + " during a previous attempt."
345                 + " This failure was cached in the local repository and"
346                 + " resolution is not be reattempted until the update interval of " + repository.getId()
347                 + " has elapsed or updates are forced", true );
348         }
349         else
350         {
351             return new MetadataTransferException( metadata, repository, metadata + " failed to transfer from "
352                 + repository.getUrl() + " during a previous attempt."
353                 + " This failure was cached in the local repository and"
354                 + " resolution will not be reattempted until the update interval of " + repository.getId()
355                 + " has elapsed or updates are forced. Original error: " + error, true );
356         }
357     }
358 
359     private long getLastUpdated( Properties props, String key )
360     {
361         String value = props.getProperty( key + UPDATED_KEY_SUFFIX, "" );
362         try
363         {
364             return ( value.length() > 0 ) ? Long.parseLong( value ) : 1;
365         }
366         catch ( NumberFormatException e )
367         {
368             LOGGER.debug( "Cannot parse last updated date {}, ignoring it", value, e );
369             return 1;
370         }
371     }
372 
373     private String getError( Properties props, String key )
374     {
375         return props.getProperty( key + ERROR_KEY_SUFFIX );
376     }
377 
378     private File getArtifactTouchFile( File artifactFile )
379     {
380         return new File( artifactFile.getPath() + UPDATED_KEY_SUFFIX );
381     }
382 
383     private File getMetadataTouchFile( File metadataFile )
384     {
385         return new File( metadataFile.getParent(), "resolver-status.properties" );
386     }
387 
388     private String getDataKey( RemoteRepository repository )
389     {
390         Set<String> mirroredUrls = Collections.emptySet();
391         if ( repository.isRepositoryManager() )
392         {
393             mirroredUrls = new TreeSet<>();
394             for ( RemoteRepository mirroredRepository : repository.getMirroredRepositories() )
395             {
396                 mirroredUrls.add( normalizeRepoUrl( mirroredRepository.getUrl() ) );
397             }
398         }
399 
400         StringBuilder buffer = new StringBuilder( 1024 );
401 
402         buffer.append( normalizeRepoUrl( repository.getUrl() ) );
403         for ( String mirroredUrl : mirroredUrls )
404         {
405             buffer.append( '+' ).append( mirroredUrl );
406         }
407 
408         return buffer.toString();
409     }
410 
411     private String getTransferKey( RepositorySystemSession session, RemoteRepository repository )
412     {
413         return getRepoKey( session, repository );
414     }
415 
416     private String getDataKey( File metadataFile )
417     {
418         return metadataFile.getName();
419     }
420 
421     private String getTransferKey( RepositorySystemSession session, File metadataFile,
422                                   RemoteRepository repository )
423     {
424         return metadataFile.getName() + '/' + getRepoKey( session, repository );
425     }
426 
427     private String getRepoKey( RepositorySystemSession session, RemoteRepository repository )
428     {
429         StringBuilder buffer = new StringBuilder( 128 );
430 
431         Proxy proxy = repository.getProxy();
432         if ( proxy != null )
433         {
434             buffer.append( AuthenticationDigest.forProxy( session, repository ) ).append( '@' );
435             buffer.append( proxy.getHost() ).append( ':' ).append( proxy.getPort() ).append( '>' );
436         }
437 
438         buffer.append( AuthenticationDigest.forRepository( session, repository ) ).append( '@' );
439 
440         buffer.append( repository.getContentType() ).append( '-' );
441         buffer.append( repository.getId() ).append( '-' );
442         buffer.append( normalizeRepoUrl( repository.getUrl() ) );
443 
444         return buffer.toString();
445     }
446 
447     private String normalizeRepoUrl( String url )
448     {
449         String result = url;
450         if ( url != null && url.length() > 0 && !url.endsWith( "/" ) )
451         {
452             result = url + '/';
453         }
454         return result;
455     }
456 
457     private String getUpdateKey( RepositorySystemSession session, File file, RemoteRepository repository )
458     {
459         return file.getAbsolutePath() + '|' + getRepoKey( session, repository );
460     }
461 
462     private int getSessionState( RepositorySystemSession session )
463     {
464         String mode = ConfigUtils.getString( session, "enabled", CONFIG_PROP_SESSION_STATE );
465         if ( Boolean.parseBoolean( mode ) || "enabled".equalsIgnoreCase( mode ) )
466         {
467             // perform update check at most once per session, regardless of update policy
468             return STATE_ENABLED;
469         }
470         else if ( "bypass".equalsIgnoreCase( mode ) )
471         {
472             // evaluate update policy but record update in session to prevent potential future checks
473             return STATE_BYPASS;
474         }
475         else
476         {
477             // no session state at all, always evaluate update policy
478             return STATE_DISABLED;
479         }
480     }
481 
482     private boolean isAlreadyUpdated( RepositorySystemSession session, Object updateKey )
483     {
484         if ( getSessionState( session ) >= STATE_BYPASS )
485         {
486             return false;
487         }
488         SessionData data = session.getData();
489         Object checkedFiles = data.get( SESSION_CHECKS );
490         if ( !( checkedFiles instanceof Map ) )
491         {
492             return false;
493         }
494         return ( (Map<?, ?>) checkedFiles ).containsKey( updateKey );
495     }
496 
497     @SuppressWarnings( "unchecked" )
498     private void setUpdated( RepositorySystemSession session, Object updateKey )
499     {
500         if ( getSessionState( session ) >= STATE_DISABLED )
501         {
502             return;
503         }
504         SessionData data = session.getData();
505         Object checkedFiles = data.get( SESSION_CHECKS );
506         while ( !( checkedFiles instanceof Map ) )
507         {
508             Object old = checkedFiles;
509             checkedFiles = new ConcurrentHashMap<>( 256 );
510             if ( data.set( SESSION_CHECKS, old, checkedFiles ) )
511             {
512                 break;
513             }
514             checkedFiles = data.get( SESSION_CHECKS );
515         }
516         ( (Map<Object, Boolean>) checkedFiles ).put( updateKey, Boolean.TRUE );
517     }
518 
519     private boolean isUpdatedRequired( RepositorySystemSession session, long lastModified, String policy )
520     {
521         return updatePolicyAnalyzer.isUpdatedRequired( session, lastModified, policy );
522     }
523 
524     private Properties read( File touchFile )
525     {
526         Properties props = trackingFileManager.read( touchFile );
527         return ( props != null ) ? props : new Properties();
528     }
529 
530     public void touchArtifact( RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check )
531     {
532         requireNonNull( session, "session cannot be null" );
533         requireNonNull( check, "check cannot be null" );
534         File artifactFile = check.getFile();
535         File touchFile = getArtifactTouchFile( artifactFile );
536 
537         String updateKey = getUpdateKey( session, artifactFile, check.getRepository() );
538         String dataKey = getDataKey( check.getAuthoritativeRepository() );
539         String transferKey = getTransferKey( session, check.getRepository() );
540 
541         setUpdated( session, updateKey );
542         Properties props = write( touchFile, dataKey, transferKey, check.getException() );
543 
544         if ( artifactFile.exists() && !hasErrors( props ) )
545         {
546             touchFile.delete();
547         }
548     }
549 
550     private boolean hasErrors( Properties props )
551     {
552         for ( Object key : props.keySet() )
553         {
554             if ( key.toString().endsWith( ERROR_KEY_SUFFIX ) )
555             {
556                 return true;
557             }
558         }
559         return false;
560     }
561 
562     public void touchMetadata( RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check )
563     {
564         requireNonNull( session, "session cannot be null" );
565         requireNonNull( check, "check cannot be null" );
566         File metadataFile = check.getFile();
567         File touchFile = getMetadataTouchFile( metadataFile );
568 
569         String updateKey = getUpdateKey( session, metadataFile, check.getRepository() );
570         String dataKey = getDataKey( metadataFile );
571         String transferKey = getTransferKey( session, metadataFile, check.getRepository() );
572 
573         setUpdated( session, updateKey );
574         write( touchFile, dataKey, transferKey, check.getException() );
575     }
576 
577     private Properties write( File touchFile, String dataKey, String transferKey, Exception error )
578     {
579         Map<String, String> updates = new HashMap<>();
580 
581         String timestamp = Long.toString( System.currentTimeMillis() );
582 
583         if ( error == null )
584         {
585             updates.put( dataKey + ERROR_KEY_SUFFIX, null );
586             updates.put( dataKey + UPDATED_KEY_SUFFIX, timestamp );
587             updates.put( transferKey + UPDATED_KEY_SUFFIX, null );
588         }
589         else if ( error instanceof ArtifactNotFoundException || error instanceof MetadataNotFoundException )
590         {
591             updates.put( dataKey + ERROR_KEY_SUFFIX, NOT_FOUND );
592             updates.put( dataKey + UPDATED_KEY_SUFFIX, timestamp );
593             updates.put( transferKey + UPDATED_KEY_SUFFIX, null );
594         }
595         else
596         {
597             String msg = error.getMessage();
598             if ( msg == null || msg.isEmpty() )
599             {
600                 msg = error.getClass().getSimpleName();
601             }
602             updates.put( dataKey + ERROR_KEY_SUFFIX, msg );
603             updates.put( dataKey + UPDATED_KEY_SUFFIX, null );
604             updates.put( transferKey + UPDATED_KEY_SUFFIX, timestamp );
605         }
606 
607         return trackingFileManager.update( touchFile, updates );
608     }
609 
610 }