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