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.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       * Manages the session state, i.e. influences if the same download requests to artifacts/metadata will happen
79       * multiple times within the same RepositorySystemSession. If "enabled" will enable the session state. If "bypass"
80       * will enable bypassing (i.e. store all artifact ids/metadata ids which have been updates but not evaluating
81       * those). All other values lead to disabling the session state completely.
82       *
83       * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
84       * @configurationType {@link java.lang.String}
85       * @configurationDefaultValue {@link #DEFAULT_SESSION_STATE}
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       * This "last modified" timestamp is used when no local file is present, signaling "first attempt" to cache a file,
100      * but as it is not present, outcome is simply always "go get it".
101      * <p>
102      * Its meaning is "we never downloaded it", so go grab it.
103      */
104     private static final long TS_NEVER = 0L;
105 
106     /**
107      * This "last modified" timestamp is returned by {@link #getLastUpdated(Properties, String)} method when the
108      * timestamp entry is not found (due properties file not present or key not present in properties file, irrelevant).
109      * It means that the cached file (artifact or metadata) is present, but we cannot tell when was it downloaded. In
110      * this case, it is {@link UpdatePolicyAnalyzer} applying in-effect policy, that decide is update (re-download)
111      * needed or not. For example, if policy is "never", we should not re-download the file.
112      * <p>
113      * Its meaning is "we downloaded it, but have no idea when", so let the policy decide its fate.
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                 // last update was successful
161                 lastUpdated = artifactFile.lastModified();
162             } else {
163                 // this is the first attempt ever
164                 lastUpdated = TS_NEVER;
165             }
166         } else if (error.isEmpty()) {
167             // artifact did not exist
168             lastUpdated = getLastUpdated(props, dataKey);
169         } else {
170             // artifact could not be transferred
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                 // last update was successful
268                 lastUpdated = getLastUpdated(props, dataKey);
269             } else {
270                 // this is the first attempt ever
271                 lastUpdated = TS_NEVER;
272             }
273         } else if (error.isEmpty()) {
274             // metadata did not exist
275             lastUpdated = getLastUpdated(props, dataKey);
276         } else {
277             // metadata could not be transferred
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             // perform update check at most once per session, regardless of update policy
420             return STATE_ENABLED;
421         } else if ("bypass".equalsIgnoreCase(mode)) {
422             // evaluate update policy but record update in session to prevent potential future checks
423             return STATE_BYPASS;
424         } else {
425             // no session state at all, always evaluate update policy
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 }