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