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