001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.internal.impl;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023import javax.inject.Singleton;
024
025import java.io.File;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.Map;
029import java.util.Properties;
030import java.util.Set;
031import java.util.TreeSet;
032import java.util.concurrent.ConcurrentHashMap;
033
034import org.eclipse.aether.RepositorySystemSession;
035import org.eclipse.aether.SessionData;
036import org.eclipse.aether.artifact.Artifact;
037import org.eclipse.aether.impl.UpdateCheck;
038import org.eclipse.aether.impl.UpdateCheckManager;
039import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
040import org.eclipse.aether.metadata.Metadata;
041import org.eclipse.aether.repository.AuthenticationDigest;
042import org.eclipse.aether.repository.Proxy;
043import org.eclipse.aether.repository.RemoteRepository;
044import org.eclipse.aether.resolution.ResolutionErrorPolicy;
045import org.eclipse.aether.spi.locator.Service;
046import org.eclipse.aether.spi.locator.ServiceLocator;
047import org.eclipse.aether.transfer.ArtifactNotFoundException;
048import org.eclipse.aether.transfer.ArtifactTransferException;
049import org.eclipse.aether.transfer.MetadataNotFoundException;
050import org.eclipse.aether.transfer.MetadataTransferException;
051import org.eclipse.aether.util.ConfigUtils;
052import org.slf4j.Logger;
053import org.slf4j.LoggerFactory;
054
055import static java.util.Objects.requireNonNull;
056
057/**
058 */
059@Singleton
060@Named
061public class DefaultUpdateCheckManager implements UpdateCheckManager, Service {
062
063    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUpdatePolicyAnalyzer.class);
064
065    private TrackingFileManager trackingFileManager;
066
067    private UpdatePolicyAnalyzer updatePolicyAnalyzer;
068
069    private static final String UPDATED_KEY_SUFFIX = ".lastUpdated";
070
071    private static final String ERROR_KEY_SUFFIX = ".error";
072
073    private static final String NOT_FOUND = "";
074
075    static final Object SESSION_CHECKS = new Object() {
076        @Override
077        public String toString() {
078            return "updateCheckManager.checks";
079        }
080    };
081
082    static final String CONFIG_PROP_SESSION_STATE = "aether.updateCheckManager.sessionState";
083
084    private static final int STATE_ENABLED = 0;
085
086    private static final int STATE_BYPASS = 1;
087
088    private static final int STATE_DISABLED = 2;
089
090    /**
091     * This "last modified" timestamp is used when no local file is present, signaling "first attempt" to cache a file,
092     * but as it is not present, outcome is simply always "go get it".
093     * <p>
094     * Its meaning is "we never downloaded it", so go grab it.
095     */
096    private static final long TS_NEVER = 0L;
097
098    /**
099     * 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}