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.ConfigurationProperties;
035import org.eclipse.aether.RepositorySystemSession;
036import org.eclipse.aether.SessionData;
037import org.eclipse.aether.artifact.Artifact;
038import org.eclipse.aether.impl.UpdateCheck;
039import org.eclipse.aether.impl.UpdateCheckManager;
040import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
041import org.eclipse.aether.metadata.Metadata;
042import org.eclipse.aether.repository.AuthenticationDigest;
043import org.eclipse.aether.repository.Proxy;
044import org.eclipse.aether.repository.RemoteRepository;
045import org.eclipse.aether.resolution.ResolutionErrorPolicy;
046import org.eclipse.aether.transfer.ArtifactNotFoundException;
047import org.eclipse.aether.transfer.ArtifactTransferException;
048import org.eclipse.aether.transfer.MetadataNotFoundException;
049import org.eclipse.aether.transfer.MetadataTransferException;
050import org.eclipse.aether.util.ConfigUtils;
051import org.slf4j.Logger;
052import org.slf4j.LoggerFactory;
053
054import static java.util.Objects.requireNonNull;
055
056/**
057 */
058@Singleton
059@Named
060public class DefaultUpdateCheckManager implements UpdateCheckManager {
061
062    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUpdatePolicyAnalyzer.class);
063
064    private static final String UPDATED_KEY_SUFFIX = ".lastUpdated";
065
066    private static final String ERROR_KEY_SUFFIX = ".error";
067
068    private static final String NOT_FOUND = "";
069
070    static final Object SESSION_CHECKS = new Object() {
071        @Override
072        public String toString() {
073            return "updateCheckManager.checks";
074        }
075    };
076
077    /**
078     * Manages the session state, i.e. influences if the same download requests to artifacts/metadata will happen
079     * multiple times within the same RepositorySystemSession. If "enabled" will enable the session state. If "bypass"
080     * will enable bypassing (i.e. store all artifact ids/metadata ids which have been updates but not evaluating
081     * those). All other values lead to disabling the session state completely.
082     *
083     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
084     * @configurationType {@link java.lang.String}
085     * @configurationDefaultValue {@link #DEFAULT_SESSION_STATE}
086     */
087    public static final String CONFIG_PROP_SESSION_STATE =
088            ConfigurationProperties.PREFIX_AETHER + "updateCheckManager.sessionState";
089
090    public static final String DEFAULT_SESSION_STATE = "enabled";
091
092    private static final int STATE_ENABLED = 0;
093
094    private static final int STATE_BYPASS = 1;
095
096    private static final int STATE_DISABLED = 2;
097
098    /**
099     * This "last modified" timestamp is used when no local file is present, signaling "first attempt" to cache a file,
100     * but as it is not present, outcome is simply always "go get it".
101     * <p>
102     * Its meaning is "we never downloaded it", so go grab it.
103     */
104    private static final long TS_NEVER = 0L;
105
106    /**
107     * This "last modified" timestamp is returned by {@link #getLastUpdated(Properties, String)} method when the
108     * timestamp entry is not found (due properties file not present or key not present in properties file, irrelevant).
109     * It means that the cached file (artifact or metadata) is present, but we cannot tell when was it downloaded. In
110     * this case, it is {@link UpdatePolicyAnalyzer} applying in-effect policy, that decide is update (re-download)
111     * needed or not. For example, if policy is "never", we should not re-download the file.
112     * <p>
113     * Its meaning is "we downloaded it, but have no idea when", so let the policy decide its fate.
114     */
115    private static final long TS_UNKNOWN = 1L;
116
117    private final TrackingFileManager trackingFileManager;
118
119    private final UpdatePolicyAnalyzer updatePolicyAnalyzer;
120
121    @Inject
122    public DefaultUpdateCheckManager(
123            TrackingFileManager trackingFileManager, UpdatePolicyAnalyzer updatePolicyAnalyzer) {
124        this.trackingFileManager = requireNonNull(trackingFileManager, "tracking file manager cannot be null");
125        this.updatePolicyAnalyzer = requireNonNull(updatePolicyAnalyzer, "update policy analyzer cannot be null");
126    }
127
128    @Override
129    public void checkArtifact(RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check) {
130        requireNonNull(session, "session cannot be null");
131        requireNonNull(check, "check cannot be null");
132        final String updatePolicy = check.getArtifactPolicy();
133        if (check.getLocalLastUpdated() != 0
134                && !isUpdatedRequired(session, check.getLocalLastUpdated(), updatePolicy)) {
135            LOGGER.debug("Skipped remote request for {}, locally installed artifact up-to-date", check.getItem());
136
137            check.setRequired(false);
138            return;
139        }
140
141        Artifact artifact = check.getItem();
142        RemoteRepository repository = check.getRepository();
143
144        File artifactFile =
145                requireNonNull(check.getFile(), String.format("The artifact '%s' has no file attached", artifact));
146
147        boolean fileExists = check.isFileValid() && artifactFile.exists();
148
149        File touchFile = getArtifactTouchFile(artifactFile);
150        Properties props = read(touchFile);
151
152        String updateKey = getUpdateKey(session, artifactFile, repository);
153        String dataKey = getDataKey(repository);
154
155        String error = getError(props, dataKey);
156
157        long lastUpdated;
158        if (error == null) {
159            if (fileExists) {
160                // last update was successful
161                lastUpdated = artifactFile.lastModified();
162            } else {
163                // this is the first attempt ever
164                lastUpdated = TS_NEVER;
165            }
166        } else if (error.isEmpty()) {
167            // artifact did not exist
168            lastUpdated = getLastUpdated(props, dataKey);
169        } else {
170            // artifact could not be transferred
171            String transferKey = getTransferKey(session, repository);
172            lastUpdated = getLastUpdated(props, transferKey);
173        }
174
175        if (lastUpdated == TS_NEVER) {
176            check.setRequired(true);
177        } else if (isAlreadyUpdated(session, updateKey)) {
178            LOGGER.debug("Skipped remote request for {}, already updated during this session", check.getItem());
179
180            check.setRequired(false);
181            if (error != null) {
182                check.setException(newException(error, artifact, repository));
183            }
184        } else if (isUpdatedRequired(session, lastUpdated, updatePolicy)) {
185            check.setRequired(true);
186        } else if (fileExists) {
187            LOGGER.debug("Skipped remote request for {}, locally cached artifact up-to-date", check.getItem());
188
189            check.setRequired(false);
190        } else {
191            int errorPolicy = Utils.getPolicy(session, artifact, repository);
192            int cacheFlag = getCacheFlag(error);
193            if ((errorPolicy & cacheFlag) != 0) {
194                check.setRequired(false);
195                check.setException(newException(error, artifact, repository));
196            } else {
197                check.setRequired(true);
198            }
199        }
200    }
201
202    private static int getCacheFlag(String error) {
203        if (error == null || error.isEmpty()) {
204            return ResolutionErrorPolicy.CACHE_NOT_FOUND;
205        } else {
206            return ResolutionErrorPolicy.CACHE_TRANSFER_ERROR;
207        }
208    }
209
210    private ArtifactTransferException newException(String error, Artifact artifact, RemoteRepository repository) {
211        if (error == null || error.isEmpty()) {
212            return new ArtifactNotFoundException(
213                    artifact,
214                    repository,
215                    artifact
216                            + " was not found in " + repository.getUrl()
217                            + " during a previous attempt. This failure was"
218                            + " cached in the local repository and"
219                            + " resolution is not reattempted until the update interval of " + repository.getId()
220                            + " has elapsed or updates are forced",
221                    true);
222        } else {
223            return new ArtifactTransferException(
224                    artifact,
225                    repository,
226                    artifact + " failed to transfer from "
227                            + repository.getUrl() + " during a previous attempt. This failure"
228                            + " was cached in the local repository and"
229                            + " resolution is not reattempted until the update interval of " + repository.getId()
230                            + " has elapsed or updates are forced. Original error: " + error,
231                    true);
232        }
233    }
234
235    @Override
236    public void checkMetadata(RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check) {
237        requireNonNull(session, "session cannot be null");
238        requireNonNull(check, "check cannot be null");
239        final String updatePolicy = check.getMetadataPolicy();
240        if (check.getLocalLastUpdated() != 0
241                && !isUpdatedRequired(session, check.getLocalLastUpdated(), updatePolicy)) {
242            LOGGER.debug("Skipped remote request for {} locally installed metadata up-to-date", check.getItem());
243
244            check.setRequired(false);
245            return;
246        }
247
248        Metadata metadata = check.getItem();
249        RemoteRepository repository = check.getRepository();
250
251        File metadataFile =
252                requireNonNull(check.getFile(), String.format("The metadata '%s' has no file attached", metadata));
253
254        boolean fileExists = check.isFileValid() && metadataFile.exists();
255
256        File touchFile = getMetadataTouchFile(metadataFile);
257        Properties props = read(touchFile);
258
259        String updateKey = getUpdateKey(session, metadataFile, repository);
260        String dataKey = getDataKey(metadataFile);
261
262        String error = getError(props, dataKey);
263
264        long lastUpdated;
265        if (error == null) {
266            if (fileExists) {
267                // last update was successful
268                lastUpdated = getLastUpdated(props, dataKey);
269            } else {
270                // this is the first attempt ever
271                lastUpdated = TS_NEVER;
272            }
273        } else if (error.isEmpty()) {
274            // metadata did not exist
275            lastUpdated = getLastUpdated(props, dataKey);
276        } else {
277            // metadata could not be transferred
278            String transferKey = getTransferKey(session, metadataFile, repository);
279            lastUpdated = getLastUpdated(props, transferKey);
280        }
281
282        if (lastUpdated == TS_NEVER) {
283            check.setRequired(true);
284        } else if (isAlreadyUpdated(session, updateKey)) {
285            LOGGER.debug("Skipped remote request for {}, already updated during this session", check.getItem());
286
287            check.setRequired(false);
288            if (error != null) {
289                check.setException(newException(error, metadata, repository));
290            }
291        } else if (isUpdatedRequired(session, lastUpdated, updatePolicy)) {
292            check.setRequired(true);
293        } else if (fileExists) {
294            LOGGER.debug("Skipped remote request for {}, locally cached metadata up-to-date", check.getItem());
295
296            check.setRequired(false);
297        } else {
298            int errorPolicy = Utils.getPolicy(session, metadata, repository);
299            int cacheFlag = getCacheFlag(error);
300            if ((errorPolicy & cacheFlag) != 0) {
301                check.setRequired(false);
302                check.setException(newException(error, metadata, repository));
303            } else {
304                check.setRequired(true);
305            }
306        }
307    }
308
309    private MetadataTransferException newException(String error, Metadata metadata, RemoteRepository repository) {
310        if (error == null || error.isEmpty()) {
311            return new MetadataNotFoundException(
312                    metadata,
313                    repository,
314                    metadata + " was not found in "
315                            + repository.getUrl() + " during a previous attempt."
316                            + " This failure was cached in the local repository and"
317                            + " resolution is not be reattempted until the update interval of " + repository.getId()
318                            + " has elapsed or updates are forced",
319                    true);
320        } else {
321            return new MetadataTransferException(
322                    metadata,
323                    repository,
324                    metadata + " failed to transfer from "
325                            + repository.getUrl() + " during a previous attempt."
326                            + " This failure was cached in the local repository and"
327                            + " resolution will not be reattempted until the update interval of " + repository.getId()
328                            + " has elapsed or updates are forced. Original error: " + error,
329                    true);
330        }
331    }
332
333    private long getLastUpdated(Properties props, String key) {
334        String value = props.getProperty(key + UPDATED_KEY_SUFFIX, "");
335        try {
336            return (!value.isEmpty()) ? Long.parseLong(value) : TS_UNKNOWN;
337        } catch (NumberFormatException e) {
338            LOGGER.debug("Cannot parse last updated date {}, ignoring it", value, e);
339            return TS_UNKNOWN;
340        }
341    }
342
343    private String getError(Properties props, String key) {
344        return props.getProperty(key + ERROR_KEY_SUFFIX);
345    }
346
347    private File getArtifactTouchFile(File artifactFile) {
348        return new File(artifactFile.getPath() + UPDATED_KEY_SUFFIX);
349    }
350
351    private File getMetadataTouchFile(File metadataFile) {
352        return new File(metadataFile.getParent(), "resolver-status.properties");
353    }
354
355    private String getDataKey(RemoteRepository repository) {
356        Set<String> mirroredUrls = Collections.emptySet();
357        if (repository.isRepositoryManager()) {
358            mirroredUrls = new TreeSet<>();
359            for (RemoteRepository mirroredRepository : repository.getMirroredRepositories()) {
360                mirroredUrls.add(normalizeRepoUrl(mirroredRepository.getUrl()));
361            }
362        }
363
364        StringBuilder buffer = new StringBuilder(1024);
365
366        buffer.append(normalizeRepoUrl(repository.getUrl()));
367        for (String mirroredUrl : mirroredUrls) {
368            buffer.append('+').append(mirroredUrl);
369        }
370
371        return buffer.toString();
372    }
373
374    private String getTransferKey(RepositorySystemSession session, RemoteRepository repository) {
375        return getRepoKey(session, repository);
376    }
377
378    private String getDataKey(File metadataFile) {
379        return metadataFile.getName();
380    }
381
382    private String getTransferKey(RepositorySystemSession session, File metadataFile, RemoteRepository repository) {
383        return metadataFile.getName() + '/' + getRepoKey(session, repository);
384    }
385
386    private String getRepoKey(RepositorySystemSession session, RemoteRepository repository) {
387        StringBuilder buffer = new StringBuilder(128);
388
389        Proxy proxy = repository.getProxy();
390        if (proxy != null) {
391            buffer.append(AuthenticationDigest.forProxy(session, repository)).append('@');
392            buffer.append(proxy.getHost()).append(':').append(proxy.getPort()).append('>');
393        }
394
395        buffer.append(AuthenticationDigest.forRepository(session, repository)).append('@');
396
397        buffer.append(repository.getContentType()).append('-');
398        buffer.append(repository.getId()).append('-');
399        buffer.append(normalizeRepoUrl(repository.getUrl()));
400
401        return buffer.toString();
402    }
403
404    private String normalizeRepoUrl(String url) {
405        String result = url;
406        if (url != null && !url.isEmpty() && !url.endsWith("/")) {
407            result = url + '/';
408        }
409        return result;
410    }
411
412    private String getUpdateKey(RepositorySystemSession session, File file, RemoteRepository repository) {
413        return file.getAbsolutePath() + '|' + getRepoKey(session, repository);
414    }
415
416    private int getSessionState(RepositorySystemSession session) {
417        String mode = ConfigUtils.getString(session, DEFAULT_SESSION_STATE, CONFIG_PROP_SESSION_STATE);
418        if (Boolean.parseBoolean(mode) || "enabled".equalsIgnoreCase(mode)) {
419            // perform update check at most once per session, regardless of update policy
420            return STATE_ENABLED;
421        } else if ("bypass".equalsIgnoreCase(mode)) {
422            // evaluate update policy but record update in session to prevent potential future checks
423            return STATE_BYPASS;
424        } else {
425            // no session state at all, always evaluate update policy
426            return STATE_DISABLED;
427        }
428    }
429
430    private boolean isAlreadyUpdated(RepositorySystemSession session, Object updateKey) {
431        if (getSessionState(session) >= STATE_BYPASS) {
432            return false;
433        }
434        SessionData data = session.getData();
435        Object checkedFiles = data.get(SESSION_CHECKS);
436        if (!(checkedFiles instanceof Map)) {
437            return false;
438        }
439        return ((Map<?, ?>) checkedFiles).containsKey(updateKey);
440    }
441
442    @SuppressWarnings("unchecked")
443    private void setUpdated(RepositorySystemSession session, Object updateKey) {
444        if (getSessionState(session) >= STATE_DISABLED) {
445            return;
446        }
447        SessionData data = session.getData();
448        Object checkedFiles = data.computeIfAbsent(SESSION_CHECKS, () -> new ConcurrentHashMap<>(256));
449        ((Map<Object, Boolean>) checkedFiles).put(updateKey, Boolean.TRUE);
450    }
451
452    private boolean isUpdatedRequired(RepositorySystemSession session, long lastModified, String policy) {
453        return updatePolicyAnalyzer.isUpdatedRequired(session, lastModified, policy);
454    }
455
456    private Properties read(File touchFile) {
457        Properties props = trackingFileManager.read(touchFile);
458        return (props != null) ? props : new Properties();
459    }
460
461    @Override
462    public void touchArtifact(RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check) {
463        requireNonNull(session, "session cannot be null");
464        requireNonNull(check, "check cannot be null");
465        File artifactFile = check.getFile();
466        File touchFile = getArtifactTouchFile(artifactFile);
467
468        String updateKey = getUpdateKey(session, artifactFile, check.getRepository());
469        String dataKey = getDataKey(check.getAuthoritativeRepository());
470        String transferKey = getTransferKey(session, check.getRepository());
471
472        setUpdated(session, updateKey);
473        Properties props = write(touchFile, dataKey, transferKey, check.getException());
474
475        if (artifactFile.exists() && !hasErrors(props)) {
476            touchFile.delete();
477        }
478    }
479
480    private boolean hasErrors(Properties props) {
481        for (Object key : props.keySet()) {
482            if (key.toString().endsWith(ERROR_KEY_SUFFIX)) {
483                return true;
484            }
485        }
486        return false;
487    }
488
489    @Override
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}