View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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       * Manages the session state, i.e. influences if the same download requests to artifacts/metadata will happen
81       * multiple times within the same RepositorySystemSession. If "enabled" will enable the session state. If "bypass"
82       * will enable bypassing (i.e. store all artifact ids/metadata ids which have been updates but not evaluating
83       * those). All other values lead to disabling the session state completely.
84       *
85       * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
86       * @configurationType {@link java.lang.String}
87       * @configurationDefaultValue {@link #DEFAULT_SESSION_STATE}
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      * This "last modified" timestamp is used when no local file is present, signaling "first attempt" to cache a file,
102      * but as it is not present, outcome is simply always "go get it".
103      * <p>
104      * Its meaning is "we never downloaded it", so go grab it.
105      */
106     private static final long TS_NEVER = 0L;
107 
108     /**
109      * This "last modified" timestamp is returned by {@link #getLastUpdated(Properties, String)} method when the
110      * timestamp entry is not found (due properties file not present or key not present in properties file, irrelevant).
111      * It means that the cached file (artifact or metadata) is present, but we cannot tell when was it downloaded. In
112      * this case, it is {@link UpdatePolicyAnalyzer} applying in-effect policy, that decide is update (re-download)
113      * needed or not. For example, if policy is "never", we should not re-download the file.
114      * <p>
115      * Its meaning is "we downloaded it, but have no idea when", so let the policy decide its fate.
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                 // last update was successful
168                 lastUpdated = pathProcessor.lastModified(artifactPath, 0L);
169             } else {
170                 // this is the first attempt ever
171                 lastUpdated = TS_NEVER;
172             }
173         } else if (error.isEmpty()) {
174             // artifact did not exist
175             lastUpdated = getLastUpdated(props, dataKey);
176         } else {
177             // artifact could not be transferred
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                 // last update was successful
275                 lastUpdated = getLastUpdated(props, dataKey);
276             } else {
277                 // this is the first attempt ever
278                 lastUpdated = TS_NEVER;
279             }
280         } else if (error.isEmpty()) {
281             // metadata did not exist
282             lastUpdated = getLastUpdated(props, dataKey);
283         } else {
284             // metadata could not be transferred
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             // perform update check at most once per session, regardless of update policy
427             return STATE_ENABLED;
428         } else if ("bypass".equalsIgnoreCase(mode)) {
429             // evaluate update policy but record update in session to prevent potential future checks
430             return STATE_BYPASS;
431         } else {
432             // no session state at all, always evaluate update policy
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 }