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