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