1 package org.eclipse.aether.internal.impl;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
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
159 lastUpdated = artifactFile.lastModified();
160 }
161 else
162 {
163
164 lastUpdated = 0L;
165 }
166 }
167 else if ( error.isEmpty() )
168 {
169
170 lastUpdated = getLastUpdated( props, dataKey );
171 }
172 else
173 {
174
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
286 lastUpdated = getLastUpdated( props, dataKey );
287 }
288 else
289 {
290
291 lastUpdated = 0L;
292 }
293 }
294 else if ( error.isEmpty() )
295 {
296
297 lastUpdated = getLastUpdated( props, dataKey );
298 }
299 else
300 {
301
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
475 return STATE_ENABLED;
476 }
477 else if ( "bypass".equalsIgnoreCase( mode ) )
478 {
479
480 return STATE_BYPASS;
481 }
482 else
483 {
484
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 }