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.ArrayList;
022import java.util.Comparator;
023import java.util.Locale;
024import java.util.SortedSet;
025import java.util.TreeSet;
026
027import org.eclipse.aether.repository.ArtifactRepository;
028import org.eclipse.aether.repository.RemoteRepository;
029import org.eclipse.aether.repository.RepositoryKeyFunction;
030import org.eclipse.aether.util.PathUtils;
031import org.eclipse.aether.util.StringDigestUtil;
032
033/**
034 * Helper class for {@link ArtifactRepository#getId()} handling. This class provides  helper methods
035 * to get id of repository as it was originally envisioned: as path safe, unique, etc. While POMs are validated by Maven,
036 * there are POMs out there that somehow define repositories with unsafe characters in their id. The problem affects mostly
037 * {@link RemoteRepository} instances, as all other implementations have fixed ids that are path safe.
038 * <p>
039 * <em>Important:</em> multiple of these provided methods are not trivial processing-wise, and some sort of
040 * caching is warmly recommended.
041 *
042 * @see PathUtils
043 * @since 2.0.11
044 */
045public final class RepositoryIdHelper {
046    private RepositoryIdHelper() {}
047
048    /**
049     * Supported {@code repositoryKey} types.
050     *
051     * @since 2.0.14
052     */
053    public enum RepositoryKeyType {
054        /**
055         * The "simple" repository key, was default in Maven 3.
056         */
057        SIMPLE,
058        /**
059         * Crafts repository key using normalized {@link RemoteRepository#getId()}.
060         */
061        NID,
062        /**
063         * Crafts repository key using hashed {@link RemoteRepository#getUrl()}.
064         */
065        HURL,
066        /**
067         * Crafts unique repository key using normalized {@link RemoteRepository#getId()} and hashed {@link RemoteRepository#getUrl()}.
068         */
069        NID_HURL,
070        /**
071         * Crafts normalized unique repository key using {@link RemoteRepository#getId()} and all the remaining properties of
072         * {@link RemoteRepository} ignoring actual list of mirrors, if any (but mirrors are split).
073         */
074        NGURK,
075        /**
076         * Crafts unique repository key using {@link RemoteRepository#getId()} and all the remaining properties of
077         * {@link RemoteRepository}.
078         */
079        GURK
080    }
081
082    /**
083     * Selector method for {@link RepositoryKeyFunction} based on string representation of {@link RepositoryKeyType}
084     * enum.
085     */
086    public static RepositoryKeyFunction getRepositoryKeyFunction(String keyTypeString) {
087        RepositoryKeyType keyType = RepositoryKeyType.valueOf(keyTypeString.toUpperCase(Locale.ENGLISH));
088        switch (keyType) {
089            case SIMPLE:
090                return RepositoryIdHelper::simpleRepositoryKey;
091            case NID:
092                return RepositoryIdHelper::nidRepositoryKey;
093            case HURL:
094                return RepositoryIdHelper::hurlRepositoryKey;
095            case NID_HURL:
096                return RepositoryIdHelper::nidAndHurlRepositoryKey;
097            case NGURK:
098                return RepositoryIdHelper::normalizedGloballyUniqueRepositoryKey;
099            case GURK:
100                return RepositoryIdHelper::globallyUniqueRepositoryKey;
101            default:
102                throw new IllegalArgumentException("Unknown repository key type: " + keyType.name());
103        }
104    }
105
106    /**
107     * Simple {@code repositoryKey} function (classic). Returns {@link RemoteRepository#getId()}, unless
108     * {@link RemoteRepository#isRepositoryManager()} returns {@code true}, in which case this method creates
109     * unique identifier based on ID and current configuration of the remote repository and context.
110     * <p>
111     * This was the default {@code repositoryKey} method in Maven 3. Is exposed (others key methods are private) as
112     * it is directly used by "simple" LRM.
113     *
114     * @since 2.0.14
115     **/
116    public static String simpleRepositoryKey(RemoteRepository repository, String context) {
117        if (repository.isRepositoryManager()) {
118            StringBuilder buffer = new StringBuilder(128);
119            buffer.append(idToPathSegment(repository));
120            buffer.append('-');
121            SortedSet<String> subKeys = new TreeSet<>();
122            for (RemoteRepository mirroredRepo : repository.getMirroredRepositories()) {
123                subKeys.add(mirroredRepo.getId());
124            }
125            StringDigestUtil sha1 = StringDigestUtil.sha1();
126            sha1.update(context);
127            for (String subKey : subKeys) {
128                sha1.update(subKey);
129            }
130            buffer.append(sha1.digest());
131            return buffer.toString();
132        } else {
133            return idToPathSegment(repository);
134        }
135    }
136
137    /**
138     * The ID {@code repositoryKey} function that uses only the {@link RemoteRepository#getId()} value to derive a key.
139     *
140     * @since 2.0.14
141     **/
142    private static String nidRepositoryKey(RemoteRepository repository, String context) {
143        String seed = null;
144        if (repository.isRepositoryManager() && context != null && !context.isEmpty()) {
145            seed += context;
146        }
147        return idToPathSegment(repository) + (seed == null ? "" : "-" + StringDigestUtil.sha1(seed));
148    }
149
150    /**
151     * The URL {@code repositoryKey} function that uses only the {@link RemoteRepository#getUrl()} hash to derive a key.
152     *
153     * @since 2.0.14
154     **/
155    private static String hurlRepositoryKey(RemoteRepository repository, String context) {
156        String seed = null;
157        if (repository.isRepositoryManager() && context != null && !context.isEmpty()) {
158            seed += context;
159        }
160        return StringDigestUtil.sha1(repository.getUrl()) + (seed == null ? "" : "-" + StringDigestUtil.sha1(seed));
161    }
162
163    /**
164     * The ID and URL {@code repositoryKey} function. This method creates unique identifier based on ID and URL
165     * of the remote repository.
166     *
167     * @since 2.0.14
168     **/
169    private static String nidAndHurlRepositoryKey(RemoteRepository repository, String context) {
170        String seed = repository.getUrl();
171        if (repository.isRepositoryManager() && context != null && !context.isEmpty()) {
172            seed += context;
173        }
174        return idToPathSegment(repository) + "-" + StringDigestUtil.sha1(seed);
175    }
176
177    /**
178     * Normalized globally unique {@code repositoryKey} function. This method creates unique identifier based on ID and current
179     * configuration of the remote repository ignoring mirrors (it records the fact repository is a mirror, but ignores
180     * mirrored repositories). If {@link RemoteRepository#isRepositoryManager()} returns {@code true}, the passed in
181     * {@code context} string is factored in as well.
182     *
183     * @since 2.0.14
184     **/
185    private static String normalizedGloballyUniqueRepositoryKey(RemoteRepository repository, String context) {
186        String seed = remoteRepositoryDescription(repository, false);
187        if (repository.isRepositoryManager() && context != null && !context.isEmpty()) {
188            seed += context;
189        }
190        return idToPathSegment(repository) + "-" + StringDigestUtil.sha1(seed);
191    }
192
193    /**
194     * Globally unique {@code repositoryKey} function. This method creates unique identifier based on ID and current
195     * configuration of the remote repository. If {@link RemoteRepository#isRepositoryManager()} returns {@code true},
196     * the passed in {@code context} string is factored in as well.
197     * <p>
198     * <em>Important:</em> this repository key can be considered "stable" for normal remote repositories (where only
199     * ID and URL matters). But, for mirror repositories, the key will change if mirror members change.
200     *
201     * @since 2.0.14
202     **/
203    private static String globallyUniqueRepositoryKey(RemoteRepository repository, String context) {
204        String seed = remoteRepositoryDescription(repository, true);
205        if (repository.isRepositoryManager() && context != null && !context.isEmpty()) {
206            seed += context;
207        }
208        return idToPathSegment(repository) + "-" + StringDigestUtil.sha1(seed);
209    }
210
211    /**
212     * This method returns the passed in {@link ArtifactRepository#getId()} value, modifying it if needed, making sure that
213     * returned repository ID is "path segment" safe. Ideally, this method should never modify repository ID, as
214     * Maven validation prevents use of illegal FS characters in them, but we found in Maven Central several POMs that
215     * define remote repositories with illegal FS characters in their ID.
216     */
217    private static String idToPathSegment(ArtifactRepository repository) {
218        if (repository instanceof RemoteRepository) {
219            return PathUtils.stringToPathSegment(repository.getId());
220        } else {
221            return repository.getId();
222        }
223    }
224
225    /**
226     * Creates unique string for given {@link RemoteRepository}. Ignores following properties:
227     * <ul>
228     *     <li>{@link RemoteRepository#getAuthentication()}</li>
229     *     <li>{@link RemoteRepository#getProxy()}</li>
230     *     <li>{@link RemoteRepository#getIntent()}</li>
231     * </ul>
232     */
233    private static String remoteRepositoryDescription(RemoteRepository repository, boolean mirrorDetails) {
234        StringBuilder buffer = new StringBuilder(256);
235        buffer.append(repository.getId());
236        buffer.append(" (").append(repository.getUrl());
237        buffer.append(", ").append(repository.getContentType());
238        boolean r = repository.getPolicy(false).isEnabled(),
239                s = repository.getPolicy(true).isEnabled();
240        if (r && s) {
241            buffer.append(", releases+snapshots");
242        } else if (r) {
243            buffer.append(", releases");
244        } else if (s) {
245            buffer.append(", snapshots");
246        } else {
247            buffer.append(", disabled");
248        }
249        if (repository.isRepositoryManager()) {
250            buffer.append(", managed");
251        }
252        if (!repository.getMirroredRepositories().isEmpty()) {
253            if (mirrorDetails) {
254                // sort them to make it stable ordering
255                ArrayList<RemoteRepository> mirroredRepositories =
256                        new ArrayList<>(repository.getMirroredRepositories());
257                mirroredRepositories.sort(Comparator.comparing(RemoteRepository::getId));
258                buffer.append(", mirrorOf(");
259                for (RemoteRepository mirroredRepo : mirroredRepositories) {
260                    buffer.append(remoteRepositoryDescription(mirroredRepo, true));
261                }
262                buffer.append(")");
263            } else {
264                buffer.append(", isMirror");
265            }
266        }
267        if (repository.isBlocked()) {
268            buffer.append(", blocked");
269        }
270        buffer.append(")");
271        return buffer.toString();
272    }
273}