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