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