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