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