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