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.Collections;
023import java.util.List;
024
025import org.eclipse.aether.repository.MirrorSelector;
026import org.eclipse.aether.repository.RemoteRepository;
027
028import static java.util.Objects.requireNonNull;
029
030/**
031 * A simple mirror selector that selects mirrors based on repository identifiers.
032 */
033public final class DefaultMirrorSelector implements MirrorSelector {
034
035    private static final String WILDCARD = "*";
036
037    private static final String EXTERNAL_WILDCARD = "external:*";
038
039    private static final String EXTERNAL_HTTP_WILDCARD = "external:http:*";
040
041    private final List<MirrorDef> mirrors = new ArrayList<>();
042
043    /**
044     * Adds the specified mirror to this selector.
045     *
046     * @param id The identifier of the mirror, must not be {@code null}.
047     * @param url The URL of the mirror, must not be {@code null}.
048     * @param type The content type of the mirror, must not be {@code null}.
049     * @param repositoryManager A flag whether the mirror is a repository manager or a simple server.
050     * @param blocked A flag whether the mirror is blocked from performing any download requests.
051     * @param mirrorOfIds The identifier(s) of remote repositories to mirror, must not be {@code null}. Multiple
052     *            identifiers can be separated by comma and additionally the wildcards "*", "external:http:*" and
053     *            "external:*" can be used to match all (external) repositories, prefixing a repo id with an
054     *            exclamation mark allows to express an exclusion. For example "external:*,!central".
055     * @param mirrorOfTypes The content type(s) of remote repositories to mirror, may be {@code null} or empty to match
056     *            any content type. Similar to the repo id specification, multiple types can be comma-separated, the
057     *            wildcard "*" and the "!" negation syntax are supported. For example "*,!p2".
058     * @return This selector for chaining, never {@code null}.
059     */
060    public DefaultMirrorSelector add(
061            String id,
062            String url,
063            String type,
064            boolean repositoryManager,
065            boolean blocked,
066            String mirrorOfIds,
067            String mirrorOfTypes) {
068        mirrors.add(new MirrorDef(id, url, type, repositoryManager, blocked, mirrorOfIds, mirrorOfTypes));
069
070        return this;
071    }
072
073    public RemoteRepository getMirror(RemoteRepository repository) {
074        requireNonNull(repository, "repository cannot be null");
075        MirrorDef mirror = findMirror(repository);
076
077        if (mirror == null) {
078            return null;
079        }
080
081        RemoteRepository.Builder builder =
082                new RemoteRepository.Builder(mirror.id, repository.getContentType(), mirror.url);
083
084        builder.setRepositoryManager(mirror.repositoryManager);
085
086        builder.setBlocked(mirror.blocked);
087
088        if (mirror.type != null && !mirror.type.isEmpty()) {
089            builder.setContentType(mirror.type);
090        }
091
092        builder.setSnapshotPolicy(repository.getPolicy(true));
093        builder.setReleasePolicy(repository.getPolicy(false));
094
095        builder.setMirroredRepositories(Collections.singletonList(repository));
096
097        return builder.build();
098    }
099
100    private MirrorDef findMirror(RemoteRepository repository) {
101        String repoId = repository.getId();
102
103        if (repoId != null && !mirrors.isEmpty()) {
104            for (MirrorDef mirror : mirrors) {
105                if (repoId.equals(mirror.mirrorOfIds)
106                        && matchesType(repository.getContentType(), mirror.mirrorOfTypes)) {
107                    return mirror;
108                }
109            }
110
111            for (MirrorDef mirror : mirrors) {
112                if (matchPattern(repository, mirror.mirrorOfIds)
113                        && matchesType(repository.getContentType(), mirror.mirrorOfTypes)) {
114                    return mirror;
115                }
116            }
117        }
118
119        return null;
120    }
121
122    /**
123     * This method checks if the pattern matches the originalRepository. Valid patterns:
124     * <ul>
125     * <li>{@code *} = everything,</li>
126     * <li>{@code external:*} = everything not on the localhost and not file based,</li>
127     * <li>{@code external:http:*} = any repository not on the localhost using HTTP,</li>
128     * <li>{@code repo,repo1} = {@code repo} or {@code repo1},</li>
129     * <li>{@code *,!repo1} = everything except {@code repo1}.</li>
130     * </ul>
131     *
132     * @param repository to compare for a match.
133     * @param pattern used for match.
134     * @return true if the repository is a match to this pattern.
135     */
136    static boolean matchPattern(RemoteRepository repository, String pattern) {
137        boolean result = false;
138        String originalId = repository.getId();
139
140        // simple checks first to short circuit processing below.
141        if (WILDCARD.equals(pattern) || pattern.equals(originalId)) {
142            result = true;
143        } else {
144            // process the list
145            String[] repos = pattern.split(",");
146            for (String repo : repos) {
147                // see if this is a negative match
148                if (repo.length() > 1 && repo.startsWith("!")) {
149                    if (repo.substring(1).equals(originalId)) {
150                        // explicitly exclude. Set result and stop processing.
151                        result = false;
152                        break;
153                    }
154                }
155                // check for exact match
156                else if (repo.equals(originalId)) {
157                    result = true;
158                    break;
159                }
160                // check for external:*
161                else if (EXTERNAL_WILDCARD.equals(repo) && isExternalRepo(repository)) {
162                    result = true;
163                    // don't stop processing in case a future segment explicitly excludes this repo
164                }
165                // check for external:http:*
166                else if (EXTERNAL_HTTP_WILDCARD.equals(repo) && isExternalHttpRepo(repository)) {
167                    result = true;
168                    // don't stop processing in case a future segment explicitly excludes this repo
169                } else if (WILDCARD.equals(repo)) {
170                    result = true;
171                    // don't stop processing in case a future segment explicitly excludes this repo
172                }
173            }
174        }
175        return result;
176    }
177
178    /**
179     * Checks the URL to see if this repository refers to an external repository.
180     *
181     * @param repository The repository to check, must not be {@code null}.
182     * @return {@code true} if external, {@code false} otherwise.
183     */
184    static boolean isExternalRepo(RemoteRepository repository) {
185        boolean local = isLocal(repository.getHost()) || "file".equalsIgnoreCase(repository.getProtocol());
186        return !local;
187    }
188
189    private static boolean isLocal(String host) {
190        return "localhost".equals(host) || "127.0.0.1".equals(host);
191    }
192
193    /**
194     * Checks the URL to see if this repository refers to a non-localhost repository using HTTP.
195     *
196     * @param repository The repository to check, must not be {@code null}.
197     * @return {@code true} if external, {@code false} otherwise.
198     */
199    static boolean isExternalHttpRepo(RemoteRepository repository) {
200        return ("http".equalsIgnoreCase(repository.getProtocol())
201                        || "dav".equalsIgnoreCase(repository.getProtocol())
202                        || "dav:http".equalsIgnoreCase(repository.getProtocol())
203                        || "dav+http".equalsIgnoreCase(repository.getProtocol()))
204                && !isLocal(repository.getHost());
205    }
206
207    /**
208     * Checks whether the types configured for a mirror match with the type of the repository.
209     *
210     * @param repoType The type of the repository, may be {@code null}.
211     * @param mirrorType The types supported by the mirror, may be {@code null}.
212     * @return {@code true} if the types associated with the mirror match the type of the original repository,
213     *         {@code false} otherwise.
214     */
215    static boolean matchesType(String repoType, String mirrorType) {
216        boolean result = false;
217
218        // simple checks first to short circuit processing below.
219        if (mirrorType == null || mirrorType.isEmpty() || WILDCARD.equals(mirrorType)) {
220            result = true;
221        } else if (mirrorType.equals(repoType)) {
222            result = true;
223        } else {
224            // process the list
225            String[] layouts = mirrorType.split(",");
226            for (String layout : layouts) {
227                // see if this is a negative match
228                if (layout.length() > 1 && layout.startsWith("!")) {
229                    if (layout.substring(1).equals(repoType)) {
230                        // explicitly exclude. Set result and stop processing.
231                        result = false;
232                        break;
233                    }
234                }
235                // check for exact match
236                else if (layout.equals(repoType)) {
237                    result = true;
238                    break;
239                } else if (WILDCARD.equals(layout)) {
240                    result = true;
241                    // don't stop processing in case a future segment explicitly excludes this repo
242                }
243            }
244        }
245
246        return result;
247    }
248
249    static class MirrorDef {
250
251        final String id;
252
253        final String url;
254
255        final String type;
256
257        final boolean repositoryManager;
258
259        final boolean blocked;
260
261        final String mirrorOfIds;
262
263        final String mirrorOfTypes;
264
265        MirrorDef(
266                String id,
267                String url,
268                String type,
269                boolean repositoryManager,
270                boolean blocked,
271                String mirrorOfIds,
272                String mirrorOfTypes) {
273            this.id = id;
274            this.url = url;
275            this.type = type;
276            this.repositoryManager = repositoryManager;
277            this.blocked = blocked;
278            this.mirrorOfIds = mirrorOfIds;
279            this.mirrorOfTypes = mirrorOfTypes;
280        }
281    }
282}