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