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     public DefaultUpdateCheckManager() {
110         // default ctor for ServiceLocator
111     }
112 
113     @Inject
114     DefaultUpdateCheckManager(TrackingFileManager trackingFileManager, UpdatePolicyAnalyzer updatePolicyAnalyzer) {
115         setTrackingFileManager(trackingFileManager);
116         setUpdatePolicyAnalyzer(updatePolicyAnalyzer);
117     }
118 
119     public void initService(ServiceLocator locator) {
120         setTrackingFileManager(locator.getService(TrackingFileManager.class));
121         setUpdatePolicyAnalyzer(locator.getService(UpdatePolicyAnalyzer.class));
122     }
123 
124     public DefaultUpdateCheckManager setTrackingFileManager(TrackingFileManager trackingFileManager) {
125         this.trackingFileManager = requireNonNull(trackingFileManager);
126         return this;
127     }
128 
129     public DefaultUpdateCheckManager setUpdatePolicyAnalyzer(UpdatePolicyAnalyzer updatePolicyAnalyzer) {
130         this.updatePolicyAnalyzer = requireNonNull(updatePolicyAnalyzer, "update policy analyzer cannot be null");
131         return this;
132     }
133 
134     public void checkArtifact(RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check) {
135         requireNonNull(session, "session cannot be null");
136         requireNonNull(check, "check cannot be null");
137         if (check.getLocalLastUpdated() != 0
138                 && !isUpdatedRequired(session, check.getLocalLastUpdated(), check.getPolicy())) {
139             LOGGER.debug("Skipped remote request for {}, locally installed artifact up-to-date", check.getItem());
140 
141             check.setRequired(false);
142             return;
143         }
144 
145         Artifact artifact = check.getItem();
146         RemoteRepository repository = check.getRepository();
147 
148         File artifactFile =
149                 requireNonNull(check.getFile(), String.format("The artifact '%s' has no file attached", artifact));
150 
151         boolean fileExists = check.isFileValid() && artifactFile.exists();
152 
153         File touchFile = getArtifactTouchFile(artifactFile);
154         Properties props = read(touchFile);
155 
156         String updateKey = getUpdateKey(session, artifactFile, repository);
157         String dataKey = getDataKey(repository);
158 
159         String error = getError(props, dataKey);
160 
161         long lastUpdated;
162         if (error == null) {
163             if (fileExists) {
164                 // last update was successful
165                 lastUpdated = artifactFile.lastModified();
166             } else {
167                 // this is the first attempt ever
168                 lastUpdated = TS_NEVER;
169             }
170         } else if (error.isEmpty()) {
171             // artifact did not exist
172             lastUpdated = getLastUpdated(props, dataKey);
173         } else {
174             // artifact could not be transferred
175             String transferKey = getTransferKey(session, repository);
176             lastUpdated = getLastUpdated(props, transferKey);
177         }
178 
179         if (lastUpdated == TS_NEVER) {
180             check.setRequired(true);
181         } else if (isAlreadyUpdated(session, updateKey)) {
182             LOGGER.debug("Skipped remote request for {}, already updated during this session", check.getItem());
183 
184             check.setRequired(false);
185             if (error != null) {
186                 check.setException(newException(error, artifact, repository));
187             }
188         } else if (isUpdatedRequired(session, lastUpdated, check.getPolicy())) {
189             check.setRequired(true);
190         } else if (fileExists) {
191             LOGGER.debug("Skipped remote request for {}, locally cached artifact up-to-date", check.getItem());
192 
193             check.setRequired(false);
194         } else {
195             int errorPolicy = Utils.getPolicy(session, artifact, repository);
196             int cacheFlag = getCacheFlag(error);
197             if ((errorPolicy & cacheFlag) != 0) {
198                 check.setRequired(false);
199                 check.setException(newException(error, artifact, repository));
200             } else {
201                 check.setRequired(true);
202             }
203         }
204     }
205 
206     private static int getCacheFlag(String error) {
207         if (error == null || error.isEmpty()) {
208             return ResolutionErrorPolicy.CACHE_NOT_FOUND;
209         } else {
210             return ResolutionErrorPolicy.CACHE_TRANSFER_ERROR;
211         }
212     }
213 
214     private ArtifactTransferException newException(String error, Artifact artifact, RemoteRepository repository) {
215         if (error == null || error.isEmpty()) {
216             return new ArtifactNotFoundException(
217                     artifact,
218                     repository,
219                     artifact
220                             + " was not found in " + repository.getUrl()
221                             + " during a previous attempt. This failure was"
222                             + " cached in the local repository and"
223                             + " resolution is not reattempted until the update interval of " + repository.getId()
224                             + " has elapsed or updates are forced",
225                     true);
226         } else {
227             return new ArtifactTransferException(
228                     artifact,
229                     repository,
230                     artifact + " failed to transfer from "
231                             + repository.getUrl() + " during a previous attempt. This failure"
232                             + " was cached in the local repository and"
233                             + " resolution is not reattempted until the update interval of " + repository.getId()
234                             + " has elapsed or updates are forced. Original error: " + error,
235                     true);
236         }
237     }
238 
239     public void checkMetadata(RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check) {
240         requireNonNull(session, "session cannot be null");
241         requireNonNull(check, "check cannot be null");
242         if (check.getLocalLastUpdated() != 0
243                 && !isUpdatedRequired(session, check.getLocalLastUpdated(), check.getPolicy())) {
244             LOGGER.debug("Skipped remote request for {} locally installed metadata up-to-date", check.getItem());
245 
246             check.setRequired(false);
247             return;
248         }
249 
250         Metadata metadata = check.getItem();
251         RemoteRepository repository = check.getRepository();
252 
253         File metadataFile =
254                 requireNonNull(check.getFile(), String.format("The metadata '%s' has no file attached", metadata));
255 
256         boolean fileExists = check.isFileValid() && metadataFile.exists();
257 
258         File touchFile = getMetadataTouchFile(metadataFile);
259         Properties props = read(touchFile);
260 
261         String updateKey = getUpdateKey(session, metadataFile, repository);
262         String dataKey = getDataKey(metadataFile);
263 
264         String error = getError(props, dataKey);
265 
266         long lastUpdated;
267         if (error == null) {
268             if (fileExists) {
269                 // last update was successful
270                 lastUpdated = getLastUpdated(props, dataKey);
271             } else {
272                 // this is the first attempt ever
273                 lastUpdated = TS_NEVER;
274             }
275         } else if (error.isEmpty()) {
276             // metadata did not exist
277             lastUpdated = getLastUpdated(props, dataKey);
278         } else {
279             // metadata could not be transferred
280             String transferKey = getTransferKey(session, metadataFile, repository);
281             lastUpdated = getLastUpdated(props, transferKey);
282         }
283 
284         if (lastUpdated == TS_NEVER) {
285             check.setRequired(true);
286         } else if (isAlreadyUpdated(session, updateKey)) {
287             LOGGER.debug("Skipped remote request for {}, already updated during this session", check.getItem());
288 
289             check.setRequired(false);
290             if (error != null) {
291                 check.setException(newException(error, metadata, repository));
292             }
293         } else if (isUpdatedRequired(session, lastUpdated, check.getPolicy())) {
294             check.setRequired(true);
295         } else if (fileExists) {
296             LOGGER.debug("Skipped remote request for {}, locally cached metadata up-to-date", check.getItem());
297 
298             check.setRequired(false);
299         } else {
300             int errorPolicy = Utils.getPolicy(session, metadata, repository);
301             int cacheFlag = getCacheFlag(error);
302             if ((errorPolicy & cacheFlag) != 0) {
303                 check.setRequired(false);
304                 check.setException(newException(error, metadata, repository));
305             } else {
306                 check.setRequired(true);
307             }
308         }
309     }
310 
311     private MetadataTransferException newException(String error, Metadata metadata, RemoteRepository repository) {
312         if (error == null || error.isEmpty()) {
313             return new MetadataNotFoundException(
314                     metadata,
315                     repository,
316                     metadata + " was not found in "
317                             + repository.getUrl() + " during a previous attempt."
318                             + " This failure was cached in the local repository and"
319                             + " resolution is not be reattempted until the update interval of " + repository.getId()
320                             + " has elapsed or updates are forced",
321                     true);
322         } else {
323             return new MetadataTransferException(
324                     metadata,
325                     repository,
326                     metadata + " failed to transfer from "
327                             + repository.getUrl() + " during a previous attempt."
328                             + " This failure was cached in the local repository and"
329                             + " resolution will not be reattempted until the update interval of " + repository.getId()
330                             + " has elapsed or updates are forced. Original error: " + error,
331                     true);
332         }
333     }
334 
335     private long getLastUpdated(Properties props, String key) {
336         String value = props.getProperty(key + UPDATED_KEY_SUFFIX, "");
337         try {
338             return (value.length() > 0) ? Long.parseLong(value) : TS_UNKNOWN;
339         } catch (NumberFormatException e) {
340             LOGGER.debug("Cannot parse last updated date {}, ignoring it", value, e);
341             return TS_UNKNOWN;
342         }
343     }
344 
345     private String getError(Properties props, String key) {
346         return props.getProperty(key + ERROR_KEY_SUFFIX);
347     }
348 
349     private File getArtifactTouchFile(File artifactFile) {
350         return new File(artifactFile.getPath() + UPDATED_KEY_SUFFIX);
351     }
352 
353     private File getMetadataTouchFile(File metadataFile) {
354         return new File(metadataFile.getParent(), "resolver-status.properties");
355     }
356 
357     private String getDataKey(RemoteRepository repository) {
358         Set<String> mirroredUrls = Collections.emptySet();
359         if (repository.isRepositoryManager()) {
360             mirroredUrls = new TreeSet<>();
361             for (RemoteRepository mirroredRepository : repository.getMirroredRepositories()) {
362                 mirroredUrls.add(normalizeRepoUrl(mirroredRepository.getUrl()));
363             }
364         }
365 
366         StringBuilder buffer = new StringBuilder(1024);
367 
368         buffer.append(normalizeRepoUrl(repository.getUrl()));
369         for (String mirroredUrl : mirroredUrls) {
370             buffer.append('+').append(mirroredUrl);
371         }
372 
373         return buffer.toString();
374     }
375 
376     private String getTransferKey(RepositorySystemSession session, RemoteRepository repository) {
377         return getRepoKey(session, repository);
378     }
379 
380     private String getDataKey(File metadataFile) {
381         return metadataFile.getName();
382     }
383 
384     private String getTransferKey(RepositorySystemSession session, File metadataFile, RemoteRepository repository) {
385         return metadataFile.getName() + '/' + getRepoKey(session, repository);
386     }
387 
388     private String getRepoKey(RepositorySystemSession session, RemoteRepository repository) {
389         StringBuilder buffer = new StringBuilder(128);
390 
391         Proxy proxy = repository.getProxy();
392         if (proxy != null) {
393             buffer.append(AuthenticationDigest.forProxy(session, repository)).append('@');
394             buffer.append(proxy.getHost()).append(':').append(proxy.getPort()).append('>');
395         }
396 
397         buffer.append(AuthenticationDigest.forRepository(session, repository)).append('@');
398 
399         buffer.append(repository.getContentType()).append('-');
400         buffer.append(repository.getId()).append('-');
401         buffer.append(normalizeRepoUrl(repository.getUrl()));
402 
403         return buffer.toString();
404     }
405 
406     private String normalizeRepoUrl(String url) {
407         String result = url;
408         if (url != null && url.length() > 0 && !url.endsWith("/")) {
409             result = url + '/';
410         }
411         return result;
412     }
413 
414     private String getUpdateKey(RepositorySystemSession session, File file, RemoteRepository repository) {
415         return file.getAbsolutePath() + '|' + getRepoKey(session, repository);
416     }
417 
418     private int getSessionState(RepositorySystemSession session) {
419         String mode = ConfigUtils.getString(session, "enabled", CONFIG_PROP_SESSION_STATE);
420         if (Boolean.parseBoolean(mode) || "enabled".equalsIgnoreCase(mode)) {
421             // perform update check at most once per session, regardless of update policy
422             return STATE_ENABLED;
423         } else if ("bypass".equalsIgnoreCase(mode)) {
424             // evaluate update policy but record update in session to prevent potential future checks
425             return STATE_BYPASS;
426         } else {
427             // no session state at all, always evaluate update policy
428             return STATE_DISABLED;
429         }
430     }
431 
432     private boolean isAlreadyUpdated(RepositorySystemSession session, Object updateKey) {
433         if (getSessionState(session) >= STATE_BYPASS) {
434             return false;
435         }
436         SessionData data = session.getData();
437         Object checkedFiles = data.get(SESSION_CHECKS);
438         if (!(checkedFiles instanceof Map)) {
439             return false;
440         }
441         return ((Map<?, ?>) checkedFiles).containsKey(updateKey);
442     }
443 
444     @SuppressWarnings("unchecked")
445     private void setUpdated(RepositorySystemSession session, Object updateKey) {
446         if (getSessionState(session) >= STATE_DISABLED) {
447             return;
448         }
449         SessionData data = session.getData();
450         Object checkedFiles = data.computeIfAbsent(SESSION_CHECKS, () -> new ConcurrentHashMap<>(256));
451         ((Map<Object, Boolean>) checkedFiles).put(updateKey, Boolean.TRUE);
452     }
453 
454     private boolean isUpdatedRequired(RepositorySystemSession session, long lastModified, String policy) {
455         return updatePolicyAnalyzer.isUpdatedRequired(session, lastModified, policy);
456     }
457 
458     private Properties read(File touchFile) {
459         Properties props = trackingFileManager.read(touchFile);
460         return (props != null) ? props : new Properties();
461     }
462 
463     public void touchArtifact(RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check) {
464         requireNonNull(session, "session cannot be null");
465         requireNonNull(check, "check cannot be null");
466         File artifactFile = check.getFile();
467         File touchFile = getArtifactTouchFile(artifactFile);
468 
469         String updateKey = getUpdateKey(session, artifactFile, check.getRepository());
470         String dataKey = getDataKey(check.getAuthoritativeRepository());
471         String transferKey = getTransferKey(session, check.getRepository());
472 
473         setUpdated(session, updateKey);
474         Properties props = write(touchFile, dataKey, transferKey, check.getException());
475 
476         if (artifactFile.exists() && !hasErrors(props)) {
477             touchFile.delete();
478         }
479     }
480 
481     private boolean hasErrors(Properties props) {
482         for (Object key : props.keySet()) {
483             if (key.toString().endsWith(ERROR_KEY_SUFFIX)) {
484                 return true;
485             }
486         }
487         return false;
488     }
489 
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 }