View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.settings.validation;
20  
21  import javax.inject.Named;
22  import javax.inject.Singleton;
23  
24  import java.net.URI;
25  import java.net.URISyntaxException;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Set;
29  import java.util.regex.Pattern;
30  
31  import org.apache.maven.settings.Mirror;
32  import org.apache.maven.settings.Profile;
33  import org.apache.maven.settings.Proxy;
34  import org.apache.maven.settings.Repository;
35  import org.apache.maven.settings.Server;
36  import org.apache.maven.settings.Settings;
37  import org.apache.maven.settings.building.SettingsProblem.Severity;
38  import org.apache.maven.settings.building.SettingsProblemCollector;
39  import org.codehaus.plexus.util.StringUtils;
40  
41  /**
42   * @author Milos Kleint
43   */
44  @Named
45  @Singleton
46  public class DefaultSettingsValidator implements SettingsValidator {
47  
48      private static final String ID = "[\\w.-]+";
49      private static final Pattern ID_REGEX = Pattern.compile(ID);
50  
51      private static final String ILLEGAL_REPO_ID_CHARS = "\\/:\"<>|?*"; // ILLEGAL_FS_CHARS
52  
53      @Override
54      public void validate(Settings settings, SettingsProblemCollector problems) {
55          if (settings.isUsePluginRegistry()) {
56              addViolation(problems, Severity.WARNING, "usePluginRegistry", null, "is deprecated and has no effect.");
57          }
58  
59          List<String> pluginGroups = settings.getPluginGroups();
60  
61          if (pluginGroups != null) {
62              for (int i = 0; i < pluginGroups.size(); i++) {
63                  String pluginGroup = pluginGroups.get(i).trim();
64  
65                  if (StringUtils.isBlank(pluginGroup)) {
66                      addViolation(
67                              problems, Severity.ERROR, "pluginGroups.pluginGroup[" + i + "]", null, "must not be empty");
68                  } else if (!ID_REGEX.matcher(pluginGroup).matches()) {
69                      addViolation(
70                              problems,
71                              Severity.ERROR,
72                              "pluginGroups.pluginGroup[" + i + "]",
73                              null,
74                              "must denote a valid group id and match the pattern " + ID);
75                  }
76              }
77          }
78  
79          List<Server> servers = settings.getServers();
80  
81          if (servers != null) {
82              Set<String> serverIds = new HashSet<>();
83  
84              for (int i = 0; i < servers.size(); i++) {
85                  Server server = servers.get(i);
86  
87                  validateStringNotEmpty(problems, "servers.server[" + i + "].id", server.getId(), null);
88  
89                  if (!serverIds.add(server.getId())) {
90                      addViolation(
91                              problems,
92                              Severity.WARNING,
93                              "servers.server.id",
94                              null,
95                              "must be unique but found duplicate server with id " + server.getId());
96                  }
97              }
98  
99              for (int i = 0; i < servers.size(); i++) {
100                 Server server = servers.get(i);
101                 for (int a = 0; a < server.getAliases().size(); a++) {
102                     String alias = server.getAliases().get(a);
103                     validateStringNotEmpty(
104                             problems, "servers.server[" + i + "].aliases[" + a + "]", alias, server.getId());
105                     if (!serverIds.add(alias)) {
106                         addViolation(
107                                 problems,
108                                 Severity.WARNING,
109                                 "servers.server[" + i + "].aliases[" + a + "]",
110                                 server.getId(),
111                                 "must be unique across all server ids and aliases but found duplicate alias " + alias);
112                     }
113                 }
114 
115                 validateRepositoryOrigins(problems, server, i);
116             }
117         }
118 
119         List<Mirror> mirrors = settings.getMirrors();
120 
121         if (mirrors != null) {
122             for (Mirror mirror : mirrors) {
123                 validateStringNotEmpty(problems, "mirrors.mirror.id", mirror.getId(), mirror.getUrl());
124 
125                 validateBannedCharacters(
126                         problems, "mirrors.mirror.id", Severity.WARNING, mirror.getId(), null, ILLEGAL_REPO_ID_CHARS);
127 
128                 if ("local".equals(mirror.getId())) {
129                     addViolation(
130                             problems,
131                             Severity.WARNING,
132                             "mirrors.mirror.id",
133                             null,
134                             "must not be 'local'"
135                                     + ", this identifier is reserved for the local repository"
136                                     + ", using it for other repositories will corrupt your repository metadata.");
137                 }
138 
139                 validateStringNotEmpty(problems, "mirrors.mirror.url", mirror.getUrl(), mirror.getId());
140 
141                 validateStringNotEmpty(problems, "mirrors.mirror.mirrorOf", mirror.getMirrorOf(), mirror.getId());
142             }
143         }
144 
145         List<Profile> profiles = settings.getProfiles();
146 
147         if (profiles != null) {
148             Set<String> profileIds = new HashSet<>();
149 
150             for (Profile profile : profiles) {
151                 if (!profileIds.add(profile.getId())) {
152                     addViolation(
153                             problems,
154                             Severity.WARNING,
155                             "profiles.profile.id",
156                             null,
157                             "must be unique but found duplicate profile with id " + profile.getId());
158                 }
159 
160                 String prefix = "profiles.profile[" + profile.getId() + "].";
161 
162                 validateRepositories(problems, profile.getRepositories(), prefix + "repositories.repository");
163                 validateRepositories(
164                         problems, profile.getPluginRepositories(), prefix + "pluginRepositories.pluginRepository");
165             }
166         }
167 
168         List<Proxy> proxies = settings.getProxies();
169 
170         if (proxies != null) {
171             Set<String> proxyIds = new HashSet<>();
172 
173             for (Proxy proxy : proxies) {
174                 if (!proxyIds.add(proxy.getId())) {
175                     addViolation(
176                             problems,
177                             Severity.WARNING,
178                             "proxies.proxy.id",
179                             null,
180                             "must be unique but found duplicate proxy with id " + proxy.getId());
181                 }
182                 validateStringNotEmpty(problems, "proxies.proxy.host", proxy.getHost(), proxy.getId());
183             }
184         }
185     }
186 
187     private void validateRepositories(SettingsProblemCollector problems, List<Repository> repositories, String prefix) {
188         Set<String> repoIds = new HashSet<>();
189 
190         for (Repository repository : repositories) {
191             validateStringNotEmpty(problems, prefix + ".id", repository.getId(), repository.getUrl());
192 
193             validateBannedCharacters(
194                     problems, prefix + ".id", Severity.WARNING, repository.getId(), null, ILLEGAL_REPO_ID_CHARS);
195 
196             if ("local".equals(repository.getId())) {
197                 addViolation(
198                         problems,
199                         Severity.WARNING,
200                         prefix + ".id",
201                         null,
202                         "must not be 'local'"
203                                 + ", this identifier is reserved for the local repository"
204                                 + ", using it for other repositories will corrupt your repository metadata.");
205             }
206 
207             if (!repoIds.add(repository.getId())) {
208                 addViolation(
209                         problems,
210                         Severity.WARNING,
211                         prefix + ".id",
212                         null,
213                         "must be unique but found duplicate repository with id " + repository.getId());
214             }
215 
216             validateStringNotEmpty(problems, prefix + ".url", repository.getUrl(), repository.getId());
217 
218             if ("legacy".equals(repository.getLayout())) {
219                 addViolation(
220                         problems,
221                         Severity.WARNING,
222                         prefix + ".layout",
223                         repository.getId(),
224                         "uses the unsupported value 'legacy', artifact resolution might fail.");
225             }
226         }
227     }
228 
229     private static void validateRepositoryOrigins(SettingsProblemCollector problems, Server server, int index) {
230         for (int o = 0; o < server.getRepositoryOrigins().size(); o++) {
231             String repositoryOrigin = server.getRepositoryOrigins().get(o);
232             String fieldName = "servers.server[" + index + "].repositoryOrigins[" + o + "]";
233 
234             if (!validateStringNotEmpty(problems, fieldName, repositoryOrigin, server.getId())) {
235                 continue;
236             }
237 
238             // settings are validated before they are interpolated here, so a placeholder is not something
239             // this validator can judge
240             if (repositoryOrigin.contains("${")) {
241                 continue;
242             }
243 
244             String invalid = invalidRepositoryOriginReason(repositoryOrigin);
245             if (invalid != null) {
246                 addViolation(problems, Severity.ERROR, fieldName, server.getId(), invalid);
247                 continue;
248             }
249 
250             String ignored = ignoredRepositoryOriginPartsReason(repositoryOrigin);
251             if (ignored != null) {
252                 addViolation(problems, Severity.WARNING, fieldName, server.getId(), ignored);
253             }
254         }
255     }
256 
257     /**
258      * Parses a {@code <server><repositoryOrigins>} value the way the credential scoping does; see
259      * {@code OriginBoundAuthenticationSelector#originOf(String)} in maven-core, which is the authority
260      * on what an origin is. Returns {@code null} when the value is not a URI at all.
261      */
262     private static URI parseRepositoryOrigin(String value) {
263         try {
264             return new URI(value).parseServerAuthority();
265         } catch (URISyntaxException e) {
266             return null;
267         }
268     }
269 
270     /**
271      * @return the reason why no origin can be derived from the given value, or {@code null} if one can
272      */
273     private static String invalidRepositoryOriginReason(String value) {
274         URI uri = parseRepositoryOrigin(value);
275         if (uri == null) {
276             return "must be a repository origin of the form scheme://host[:port] but found '" + value + "'";
277         }
278         if (uri.getScheme() == null) {
279             return "must start with a scheme, for example https://repo.example.org, but found '" + value + "'";
280         }
281         if (uri.getHost() == null) {
282             return "must name a host, for example https://repo.example.org, but found '" + value + "'";
283         }
284         if (uri.getUserInfo() != null) {
285             return "must not carry user information but found '" + value + "'";
286         }
287         return null;
288     }
289 
290     /**
291      * @return the reason why parts of the given value are ignored, or {@code null} if it is a bare origin
292      */
293     private static String ignoredRepositoryOriginPartsReason(String value) {
294         URI uri = parseRepositoryOrigin(value);
295         String path = uri.getRawPath();
296         boolean extraPath = path != null && !path.isEmpty() && !"/".equals(path);
297         if (!extraPath && uri.getRawQuery() == null && uri.getRawFragment() == null) {
298             return null;
299         }
300         return "is a repository origin, not a repository URL; only '" + uri.getScheme() + "://" + uri.getAuthority()
301                 + "' of '" + value + "' is used";
302     }
303 
304     // ----------------------------------------------------------------------
305     // Field validation
306     // ----------------------------------------------------------------------
307 
308     /**
309      * Asserts:
310      * <p/>
311      * <ul>
312      * <li><code>string.length != null</code>
313      * <li><code>string.length > 0</code>
314      * </ul>
315      */
316     private static boolean validateStringNotEmpty(
317             SettingsProblemCollector problems, String fieldName, String string, String sourceHint) {
318         if (!validateNotNull(problems, fieldName, string, sourceHint)) {
319             return false;
320         }
321 
322         if (string.length() > 0) {
323             return true;
324         }
325 
326         addViolation(problems, Severity.ERROR, fieldName, sourceHint, "is missing");
327 
328         return false;
329     }
330 
331     /**
332      * Asserts:
333      * <p/>
334      * <ul>
335      * <li><code>string != null</code>
336      * </ul>
337      */
338     private static boolean validateNotNull(
339             SettingsProblemCollector problems, String fieldName, Object object, String sourceHint) {
340         if (object != null) {
341             return true;
342         }
343 
344         addViolation(problems, Severity.ERROR, fieldName, sourceHint, "is missing");
345 
346         return false;
347     }
348 
349     private static boolean validateBannedCharacters(
350             SettingsProblemCollector problems,
351             String fieldName,
352             Severity severity,
353             String string,
354             String sourceHint,
355             String banned) {
356         if (string != null) {
357             for (int i = string.length() - 1; i >= 0; i--) {
358                 if (banned.indexOf(string.charAt(i)) >= 0) {
359                     addViolation(
360                             problems,
361                             severity,
362                             fieldName,
363                             sourceHint,
364                             "must not contain any of these characters " + banned + " but found " + string.charAt(i));
365                     return false;
366                 }
367             }
368         }
369 
370         return true;
371     }
372 
373     private static void addViolation(
374             SettingsProblemCollector problems, Severity severity, String fieldName, String sourceHint, String message) {
375         StringBuilder buffer = new StringBuilder(256);
376         buffer.append('\'').append(fieldName).append('\'');
377 
378         if (sourceHint != null) {
379             buffer.append(" for ").append(sourceHint);
380         }
381 
382         buffer.append(' ').append(message);
383 
384         problems.add(severity, buffer.toString(), -1, -1, null);
385     }
386 }