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