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