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