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