1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.aether.internal.impl;
20
21 import javax.inject.Inject;
22 import javax.inject.Named;
23 import javax.inject.Singleton;
24
25 import java.nio.file.Files;
26 import java.nio.file.Path;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.Map;
30 import java.util.Properties;
31 import java.util.Set;
32 import java.util.TreeSet;
33 import java.util.concurrent.ConcurrentHashMap;
34
35 import org.eclipse.aether.ConfigurationProperties;
36 import org.eclipse.aether.Keys;
37 import org.eclipse.aether.RepositorySystemSession;
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.io.PathProcessor;
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 import static java.util.Objects.requireNonNull;
57
58
59
60 @Singleton
61 @Named
62 public class DefaultUpdateCheckManager implements UpdateCheckManager {
63
64 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUpdateCheckManager.class);
65
66 private static final String UPDATED_KEY_SUFFIX = ".lastUpdated";
67
68 private static final String ERROR_KEY_SUFFIX = ".error";
69
70 private static final String NOT_FOUND = "";
71
72
73 static final Object SESSION_CHECKS = Keys.of(new Object() {
74 @Override
75 public String toString() {
76 return "updateCheckManager.checks";
77 }
78 });
79
80
81
82
83
84
85
86
87
88
89
90 public static final String CONFIG_PROP_SESSION_STATE =
91 ConfigurationProperties.PREFIX_AETHER + "updateCheckManager.sessionState";
92
93 public static final String DEFAULT_SESSION_STATE = "enabled";
94
95 private static final int STATE_ENABLED = 0;
96
97 private static final int STATE_BYPASS = 1;
98
99 private static final int STATE_DISABLED = 2;
100
101
102
103
104
105
106
107 private static final long TS_NEVER = 0L;
108
109
110
111
112
113
114
115
116
117
118 private static final long TS_UNKNOWN = 1L;
119
120 private final TrackingFileManager trackingFileManager;
121
122 private final UpdatePolicyAnalyzer updatePolicyAnalyzer;
123
124 private final PathProcessor pathProcessor;
125
126 @Inject
127 public DefaultUpdateCheckManager(
128 TrackingFileManager trackingFileManager,
129 UpdatePolicyAnalyzer updatePolicyAnalyzer,
130 PathProcessor pathProcessor) {
131 this.trackingFileManager = requireNonNull(trackingFileManager, "tracking file manager cannot be null");
132 this.updatePolicyAnalyzer = requireNonNull(updatePolicyAnalyzer, "update policy analyzer cannot be null");
133 this.pathProcessor = requireNonNull(pathProcessor, "path processor cannot be null");
134 }
135
136 @Override
137 public void checkArtifact(RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check) {
138 requireNonNull(session, "session cannot be null");
139 requireNonNull(check, "check cannot be null");
140 final String updatePolicy = check.getArtifactPolicy();
141 if (check.getLocalLastUpdated() != 0
142 && !isUpdatedRequired(session, check.getLocalLastUpdated(), updatePolicy)) {
143 LOGGER.debug("Skipped remote request for {}, locally installed artifact up-to-date", check.getItem());
144
145 check.setRequired(false);
146 return;
147 }
148
149 Artifact artifact = check.getItem();
150 RemoteRepository repository = check.getRepository();
151
152 Path artifactPath =
153 requireNonNull(check.getPath(), String.format("The artifact '%s' has no file attached", artifact));
154
155 boolean fileExists = check.isFileValid() && Files.exists(artifactPath);
156
157 Path touchPath = getArtifactTouchFile(artifactPath);
158 Properties props = read(touchPath);
159
160 String updateKey = getUpdateKey(session, artifactPath, repository);
161 String dataKey = getDataKey(repository);
162
163 String error = getError(props, dataKey);
164
165 long lastUpdated;
166 if (error == null) {
167 if (fileExists) {
168
169 lastUpdated = pathProcessor.lastModified(artifactPath, 0L);
170 } else {
171
172 lastUpdated = TS_NEVER;
173 }
174 } else if (error.isEmpty()) {
175
176 lastUpdated = getLastUpdated(props, dataKey);
177 } else {
178
179 String transferKey = getTransferKey(session, repository);
180 lastUpdated = getLastUpdated(props, transferKey);
181 }
182
183 if (lastUpdated == TS_NEVER) {
184 check.setRequired(true);
185 } else if (isAlreadyUpdated(session, updateKey)) {
186 LOGGER.debug("Skipped remote request for {}, already updated during this session", check.getItem());
187
188 check.setRequired(false);
189 if (error != null) {
190 check.setException(newException(error, artifact, repository));
191 }
192 } else if (isUpdatedRequired(session, lastUpdated, updatePolicy)) {
193 check.setRequired(true);
194 } else if (fileExists) {
195 LOGGER.debug("Skipped remote request for {}, locally cached artifact up-to-date", check.getItem());
196
197 check.setRequired(false);
198 } else {
199 int errorPolicy = Utils.getPolicy(session, artifact, repository);
200 int cacheFlag = getCacheFlag(error);
201 if ((errorPolicy & cacheFlag) != 0) {
202 check.setRequired(false);
203 check.setException(newException(error, artifact, repository));
204 } else {
205 check.setRequired(true);
206 }
207 }
208 }
209
210 private static int getCacheFlag(String error) {
211 if (error == null || error.isEmpty()) {
212 return ResolutionErrorPolicy.CACHE_NOT_FOUND;
213 } else {
214 return ResolutionErrorPolicy.CACHE_TRANSFER_ERROR;
215 }
216 }
217
218 private ArtifactTransferException newException(String error, Artifact artifact, RemoteRepository repository) {
219 if (error == null || error.isEmpty()) {
220 return new ArtifactNotFoundException(
221 artifact,
222 repository,
223 artifact
224 + " was not found in " + repository.getUrl()
225 + " during a previous attempt. This failure was"
226 + " cached in the local repository and"
227 + " resolution is not reattempted until the update interval of " + repository.getId()
228 + " has elapsed or updates are forced",
229 true);
230 } else {
231 return new ArtifactTransferException(
232 artifact,
233 repository,
234 artifact + " failed to transfer from "
235 + repository.getUrl() + " during a previous attempt. This failure"
236 + " was cached in the local repository and"
237 + " resolution is not reattempted until the update interval of " + repository.getId()
238 + " has elapsed or updates are forced. Original error: " + error,
239 true);
240 }
241 }
242
243 @Override
244 public void checkMetadata(RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check) {
245 requireNonNull(session, "session cannot be null");
246 requireNonNull(check, "check cannot be null");
247 final String updatePolicy = check.getMetadataPolicy();
248 if (check.getLocalLastUpdated() != 0
249 && !isUpdatedRequired(session, check.getLocalLastUpdated(), updatePolicy)) {
250 LOGGER.debug("Skipped remote request for {} locally installed metadata up-to-date", check.getItem());
251
252 check.setRequired(false);
253 return;
254 }
255
256 Metadata metadata = check.getItem();
257 RemoteRepository repository = check.getRepository();
258
259 Path metadataPath =
260 requireNonNull(check.getPath(), String.format("The metadata '%s' has no file attached", metadata));
261
262 boolean fileExists = check.isFileValid() && Files.exists(metadataPath);
263
264 Path touchPath = getMetadataTouchFile(metadataPath);
265 Properties props = read(touchPath);
266
267 String updateKey = getUpdateKey(session, metadataPath, repository);
268 String dataKey = getDataKey(metadataPath);
269
270 String error = getError(props, dataKey);
271
272 long lastUpdated;
273 if (error == null) {
274 if (fileExists) {
275
276 lastUpdated = getLastUpdated(props, dataKey);
277 } else {
278
279 lastUpdated = TS_NEVER;
280 }
281 } else if (error.isEmpty()) {
282
283 lastUpdated = getLastUpdated(props, dataKey);
284 } else {
285
286 String transferKey = getTransferKey(session, metadataPath, repository);
287 lastUpdated = getLastUpdated(props, transferKey);
288 }
289
290 if (lastUpdated == TS_NEVER) {
291 check.setRequired(true);
292 } else if (isAlreadyUpdated(session, updateKey)) {
293 LOGGER.debug("Skipped remote request for {}, already updated during this session", check.getItem());
294
295 check.setRequired(false);
296 if (error != null) {
297 check.setException(newException(error, metadata, repository));
298 }
299 } else if (isUpdatedRequired(session, lastUpdated, updatePolicy)) {
300 check.setRequired(true);
301 } else if (fileExists) {
302 LOGGER.debug("Skipped remote request for {}, locally cached metadata up-to-date", check.getItem());
303
304 check.setRequired(false);
305 } else {
306 int errorPolicy = Utils.getPolicy(session, metadata, repository);
307 int cacheFlag = getCacheFlag(error);
308 if ((errorPolicy & cacheFlag) != 0) {
309 check.setRequired(false);
310 check.setException(newException(error, metadata, repository));
311 } else {
312 check.setRequired(true);
313 }
314 }
315 }
316
317 private MetadataTransferException newException(String error, Metadata metadata, RemoteRepository repository) {
318 if (error == null || error.isEmpty()) {
319 return new MetadataNotFoundException(
320 metadata,
321 repository,
322 metadata + " was not found in "
323 + repository.getUrl() + " during a previous attempt."
324 + " This failure was cached in the local repository and"
325 + " resolution is not be reattempted until the update interval of " + repository.getId()
326 + " has elapsed or updates are forced",
327 true);
328 } else {
329 return new MetadataTransferException(
330 metadata,
331 repository,
332 metadata + " failed to transfer from "
333 + repository.getUrl() + " during a previous attempt."
334 + " This failure was cached in the local repository and"
335 + " resolution will not be reattempted until the update interval of " + repository.getId()
336 + " has elapsed or updates are forced. Original error: " + error,
337 true);
338 }
339 }
340
341 private long getLastUpdated(Properties props, String key) {
342 String value = props.getProperty(key + UPDATED_KEY_SUFFIX, "");
343 try {
344 return (!value.isEmpty()) ? Long.parseLong(value) : TS_UNKNOWN;
345 } catch (NumberFormatException e) {
346 LOGGER.debug("Cannot parse last updated date {}, ignoring it", value, e);
347 return TS_UNKNOWN;
348 }
349 }
350
351 private String getError(Properties props, String key) {
352 return props.getProperty(key + ERROR_KEY_SUFFIX);
353 }
354
355 private Path getArtifactTouchFile(Path artifactPath) {
356 return artifactPath.getParent().resolve(artifactPath.getFileName() + UPDATED_KEY_SUFFIX);
357 }
358
359 private Path getMetadataTouchFile(Path metadataPath) {
360 return metadataPath.getParent().resolve("resolver-status.properties");
361 }
362
363 private String getDataKey(RemoteRepository repository) {
364 Set<String> mirroredUrls = Collections.emptySet();
365 if (repository.isRepositoryManager()) {
366 mirroredUrls = new TreeSet<>();
367 for (RemoteRepository mirroredRepository : repository.getMirroredRepositories()) {
368 mirroredUrls.add(normalizeRepoUrl(mirroredRepository.getUrl()));
369 }
370 }
371
372 StringBuilder buffer = new StringBuilder(1024);
373
374 buffer.append(normalizeRepoUrl(repository.getUrl()));
375 for (String mirroredUrl : mirroredUrls) {
376 buffer.append('+').append(mirroredUrl);
377 }
378
379 return buffer.toString();
380 }
381
382 private String getTransferKey(RepositorySystemSession session, RemoteRepository repository) {
383 return getRepoKey(session, repository);
384 }
385
386 private String getDataKey(Path metadataPath) {
387 return metadataPath.getFileName().toString();
388 }
389
390 private String getTransferKey(RepositorySystemSession session, Path metadataPath, RemoteRepository repository) {
391 return metadataPath.getFileName().toString() + '/' + getRepoKey(session, repository);
392 }
393
394 private String getRepoKey(RepositorySystemSession session, RemoteRepository repository) {
395 StringBuilder buffer = new StringBuilder(128);
396
397 Proxy proxy = repository.getProxy();
398 if (proxy != null) {
399 buffer.append(AuthenticationDigest.forProxy(session, repository)).append('@');
400 buffer.append(proxy.getHost()).append(':').append(proxy.getPort()).append('>');
401 }
402
403 buffer.append(AuthenticationDigest.forRepository(session, repository)).append('@');
404
405 buffer.append(repository.getContentType()).append('-');
406 buffer.append(repository.getId()).append('-');
407 buffer.append(normalizeRepoUrl(repository.getUrl()));
408
409 return buffer.toString();
410 }
411
412 private String normalizeRepoUrl(String url) {
413 String result = url;
414 if (url != null && !url.isEmpty() && !url.endsWith("/")) {
415 result = url + '/';
416 }
417 return result;
418 }
419
420 private String getUpdateKey(RepositorySystemSession session, Path path, RemoteRepository repository) {
421 return path.toAbsolutePath() + "|" + getRepoKey(session, repository);
422 }
423
424 private int getSessionState(RepositorySystemSession session) {
425 String mode = ConfigUtils.getString(session, DEFAULT_SESSION_STATE, CONFIG_PROP_SESSION_STATE);
426 if (Boolean.parseBoolean(mode) || "enabled".equalsIgnoreCase(mode)) {
427
428 return STATE_ENABLED;
429 } else if ("bypass".equalsIgnoreCase(mode)) {
430
431 return STATE_BYPASS;
432 } else {
433
434 return STATE_DISABLED;
435 }
436 }
437
438 private boolean isAlreadyUpdated(RepositorySystemSession session, String updateKey) {
439 if (getSessionState(session) >= STATE_BYPASS) {
440 return false;
441 }
442 Object checkedFiles = session.getData().get(SESSION_CHECKS);
443 if (!(checkedFiles instanceof Map)) {
444 return false;
445 }
446 return ((Map<?, ?>) checkedFiles).containsKey(updateKey);
447 }
448
449 @SuppressWarnings("unchecked")
450 private void setUpdated(RepositorySystemSession session, String updateKey) {
451 if (getSessionState(session) >= STATE_DISABLED) {
452 return;
453 }
454 Object checkedFiles = session.getData().computeIfAbsent(SESSION_CHECKS, () -> new ConcurrentHashMap<>(256));
455 ((Map<String, Boolean>) checkedFiles).put(updateKey, Boolean.TRUE);
456 }
457
458 private boolean isUpdatedRequired(RepositorySystemSession session, long lastModified, String policy) {
459 return updatePolicyAnalyzer.isUpdatedRequired(session, lastModified, policy);
460 }
461
462 private Properties read(Path touchPath) {
463 Properties props = trackingFileManager.read(touchPath);
464 return (props != null) ? props : new Properties();
465 }
466
467 @Override
468 public void touchArtifact(RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check) {
469 requireNonNull(session, "session cannot be null");
470 requireNonNull(check, "check cannot be null");
471 Path artifactPath = check.getPath();
472 Path touchPath = getArtifactTouchFile(artifactPath);
473
474 String updateKey = getUpdateKey(session, artifactPath, check.getRepository());
475 String dataKey = getDataKey(check.getAuthoritativeRepository());
476 String transferKey = getTransferKey(session, check.getRepository());
477
478 setUpdated(session, updateKey);
479 Properties props = write(touchPath, dataKey, transferKey, check.getException());
480
481 if (Files.exists(artifactPath) && !hasErrors(props)) {
482 trackingFileManager.delete(touchPath);
483 }
484 }
485
486 private boolean hasErrors(Properties props) {
487 for (Object key : props.keySet()) {
488 if (key.toString().endsWith(ERROR_KEY_SUFFIX)) {
489 return true;
490 }
491 }
492 return false;
493 }
494
495 @Override
496 public void touchMetadata(RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check) {
497 requireNonNull(session, "session cannot be null");
498 requireNonNull(check, "check cannot be null");
499 Path metadataPath = check.getPath();
500 Path touchPath = getMetadataTouchFile(metadataPath);
501
502 String updateKey = getUpdateKey(session, metadataPath, check.getRepository());
503 String dataKey = getDataKey(metadataPath);
504 String transferKey = getTransferKey(session, metadataPath, check.getRepository());
505
506 setUpdated(session, updateKey);
507 write(touchPath, dataKey, transferKey, check.getException());
508 }
509
510 private Properties write(Path touchPath, String dataKey, String transferKey, Exception error) {
511 Map<String, String> updates = new HashMap<>();
512
513 String timestamp = Long.toString(System.currentTimeMillis());
514
515 if (error == null) {
516 updates.put(dataKey + ERROR_KEY_SUFFIX, null);
517 updates.put(dataKey + UPDATED_KEY_SUFFIX, timestamp);
518 updates.put(transferKey + UPDATED_KEY_SUFFIX, null);
519 } else if (error instanceof ArtifactNotFoundException || error instanceof MetadataNotFoundException) {
520 updates.put(dataKey + ERROR_KEY_SUFFIX, NOT_FOUND);
521 updates.put(dataKey + UPDATED_KEY_SUFFIX, timestamp);
522 updates.put(transferKey + UPDATED_KEY_SUFFIX, null);
523 } else {
524 String msg = error.getMessage();
525 if (msg == null || msg.isEmpty()) {
526 msg = error.getClass().getSimpleName();
527 }
528 updates.put(dataKey + ERROR_KEY_SUFFIX, msg);
529 updates.put(dataKey + UPDATED_KEY_SUFFIX, null);
530 updates.put(transferKey + UPDATED_KEY_SUFFIX, timestamp);
531 }
532
533 return trackingFileManager.update(touchPath, updates);
534 }
535 }