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.util.repository;
020
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Locale;
025import java.util.concurrent.ConcurrentHashMap;
026import java.util.function.Function;
027import java.util.function.Predicate;
028
029import org.eclipse.aether.RepositorySystemSession;
030import org.eclipse.aether.repository.ArtifactRepository;
031import org.eclipse.aether.repository.RemoteRepository;
032import org.eclipse.aether.util.PathUtils;
033import org.eclipse.aether.util.StringDigestUtil;
034
035import static java.util.Objects.requireNonNull;
036
037/**
038 * Helper class for {@link ArtifactRepository#getId()} handling. This class provides  helper function (cached or uncached)
039 * to get id of repository as it was originally envisioned: as path safe. While POMs are validated by Maven, there are
040 * POMs out there that somehow define repositories with unsafe characters in their id. The problem affects mostly
041 * {@link RemoteRepository} instances, as all other implementations have fixed ids that are path safe.
042 *
043 * @see PathUtils
044 * @since 2.0.11
045 */
046public final class RepositoryIdHelper {
047    private RepositoryIdHelper() {}
048
049    private static final String CENTRAL_REPOSITORY_ID = "central";
050    private static final Collection<String> CENTRAL_URLS = Collections.unmodifiableList(Arrays.asList(
051            "https://repo.maven.apache.org/maven2",
052            "https://repo1.maven.org/maven2",
053            "https://maven-central.storage-download.googleapis.com/maven2"));
054    private static final Predicate<RemoteRepository> CENTRAL_DIRECT_ONLY =
055            remoteRepository -> CENTRAL_REPOSITORY_ID.equals(remoteRepository.getId())
056                    && "https".equals(remoteRepository.getProtocol().toLowerCase(Locale.ENGLISH))
057                    && CENTRAL_URLS.stream().anyMatch(remoteUrl -> {
058                        String rurl = remoteRepository.getUrl().toLowerCase(Locale.ENGLISH);
059                        if (rurl.endsWith("/")) {
060                            rurl = rurl.substring(0, rurl.length() - 1);
061                        }
062                        return rurl.equals(remoteUrl);
063                    })
064                    && remoteRepository.getPolicy(false).isEnabled()
065                    && !remoteRepository.getPolicy(true).isEnabled()
066                    && remoteRepository.getMirroredRepositories().isEmpty()
067                    && !remoteRepository.isRepositoryManager()
068                    && !remoteRepository.isBlocked();
069
070    /**
071     * Creates unique repository id for given {@link RemoteRepository}. For Maven Central this method will return
072     * string "central", while for any other remote repository it will return string created as
073     * {@code $(repository.id)-sha1(repository-aspects)}. The key material contains all relevant aspects
074     * of remote repository, so repository with same ID even if just policy changes (enabled/disabled), will map to
075     * different string id. The checksum and update policies are not participating in key creation.
076     * <p>
077     * This method is costly, so should be invoked sparingly, or cache results if needed.
078     */
079    public static String remoteRepositoryUniqueId(RemoteRepository repository) {
080        if (CENTRAL_DIRECT_ONLY.test(repository)) {
081            return CENTRAL_REPOSITORY_ID;
082        } else {
083            StringBuilder buffer = new StringBuilder(256);
084            buffer.append(repository.getId());
085            buffer.append(" (").append(repository.getUrl());
086            buffer.append(", ").append(repository.getContentType());
087            boolean r = repository.getPolicy(false).isEnabled(),
088                    s = repository.getPolicy(true).isEnabled();
089            if (r && s) {
090                buffer.append(", releases+snapshots");
091            } else if (r) {
092                buffer.append(", releases");
093            } else if (s) {
094                buffer.append(", snapshots");
095            } else {
096                buffer.append(", disabled");
097            }
098            if (repository.isRepositoryManager()) {
099                buffer.append(", managed(");
100                for (RemoteRepository mirroredRepo : repository.getMirroredRepositories()) {
101                    buffer.append(remoteRepositoryUniqueId(mirroredRepo));
102                }
103                buffer.append(")");
104            }
105            if (repository.isBlocked()) {
106                buffer.append(", blocked");
107            }
108            buffer.append(")");
109            return idToPathSegment(repository) + "-" + StringDigestUtil.sha1(buffer.toString());
110        }
111    }
112
113    /**
114     * Returns same instance of (session cached) function for session.
115     */
116    @SuppressWarnings("unchecked")
117    public static Function<ArtifactRepository, String> cachedIdToPathSegment(RepositorySystemSession session) {
118        requireNonNull(session, "session");
119        return (Function<ArtifactRepository, String>) session.getData()
120                .computeIfAbsent(
121                        RepositoryIdHelper.class.getSimpleName() + "-idToPathSegmentFunction",
122                        () -> cachedIdToPathSegmentFunction(session));
123    }
124
125    /**
126     * Returns new instance of function backed by cached or uncached (if session has no cache set)
127     * {@link #idToPathSegment(ArtifactRepository)} method call.
128     */
129    @SuppressWarnings("unchecked")
130    private static Function<ArtifactRepository, String> cachedIdToPathSegmentFunction(RepositorySystemSession session) {
131        if (session.getCache() != null) {
132            return repository -> ((ConcurrentHashMap<String, String>) session.getCache()
133                            .computeIfAbsent(
134                                    session,
135                                    RepositoryIdHelper.class.getSimpleName() + "-idToPathSegmentCache",
136                                    ConcurrentHashMap::new))
137                    .computeIfAbsent(repository.getId(), id -> idToPathSegment(repository));
138        } else {
139            return RepositoryIdHelper::idToPathSegment; // uncached
140        }
141    }
142
143    /**
144     * This method returns the passed in {@link ArtifactRepository#getId()} value, modifying it if needed, making sure that
145     * returned repository ID is "path segment" safe. Ideally, this method should never modify repository ID, as
146     * Maven validation prevents use of illegal FS characters in them, but we found in Maven Central several POMs that
147     * define remote repositories with illegal FS characters in their ID.
148     * <p>
149     * This method is simplistic on purpose, and if frequently used, best if results are cached (per session),
150     * see {@link #cachedIdToPathSegment(RepositorySystemSession)} method.
151     *
152     * @see #cachedIdToPathSegment(RepositorySystemSession)
153     */
154    private static String idToPathSegment(ArtifactRepository repository) {
155        if (repository instanceof RemoteRepository) {
156            return PathUtils.stringToPathSegment(repository.getId());
157        } else {
158            return repository.getId();
159        }
160    }
161}