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