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