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.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       * Manages the session state, i.e. influences if the same download requests to artifacts/metadata will happen
83       * multiple times within the same RepositorySystemSession. If "enabled" will enable the session state. If "bypass"
84       * will enable bypassing (i.e. store all artifact ids/metadata ids which have been updates but not evaluating
85       * those). All other values lead to disabling the session state completely.
86       *
87       * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
88       * @configurationType {@link java.lang.String}
89       * @configurationDefaultValue {@link #DEFAULT_SESSION_STATE}
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      * This "last modified" timestamp is used when no local file is present, signaling "first attempt" to cache a file,
104      * but as it is not present, outcome is simply always "go get it".
105      * <p>
106      * Its meaning is "we never downloaded it", so go grab it.
107      */
108     private static final long TS_NEVER = 0L;
109 
110     /**
111      * This "last modified" timestamp is returned by {@link #getLastUpdated(Properties, String)} method when the
112      * timestamp entry is not found (due properties file not present or key not present in properties file, irrelevant).
113      * It means that the cached file (artifact or metadata) is present, but we cannot tell when was it downloaded. In
114      * this case, it is {@link UpdatePolicyAnalyzer} applying in-effect policy, that decide is update (re-download)
115      * needed or not. For example, if policy is "never", we should not re-download the file.
116      * <p>
117      * Its meaning is "we downloaded it, but have no idea when", so let the policy decide its fate.
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                 // last update was successful
170                 lastUpdated = pathProcessor.lastModified(artifactPath, 0L);
171             } else {
172                 // this is the first attempt ever
173                 lastUpdated = TS_NEVER;
174             }
175         } else if (error.isEmpty()) {
176             // artifact did not exist
177             lastUpdated = getLastUpdated(props, dataKey);
178         } else {
179             // artifact could not be transferred
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                 // last update was successful
277                 lastUpdated = getLastUpdated(props, dataKey);
278             } else {
279                 // this is the first attempt ever
280                 lastUpdated = TS_NEVER;
281             }
282         } else if (error.isEmpty()) {
283             // metadata did not exist
284             lastUpdated = getLastUpdated(props, dataKey);
285         } else {
286             // metadata could not be transferred
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             // perform update check at most once per session, regardless of update policy
429             return STATE_ENABLED;
430         } else if ("bypass".equalsIgnoreCase(mode)) {
431             // evaluate update policy but record update in session to prevent potential future checks
432             return STATE_BYPASS;
433         } else {
434             // no session state at all, always evaluate update policy
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 }