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