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.internal.impl;
20  
21  import javax.xml.stream.Location;
22  import javax.xml.stream.XMLStreamException;
23  
24  import java.io.File;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.util.ArrayList;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.function.Function;
33  import java.util.function.Supplier;
34  
35  import org.apache.maven.api.di.Inject;
36  import org.apache.maven.api.di.Named;
37  import org.apache.maven.api.services.BuilderProblem;
38  import org.apache.maven.api.services.Interpolator;
39  import org.apache.maven.api.services.SettingsBuilder;
40  import org.apache.maven.api.services.SettingsBuilderException;
41  import org.apache.maven.api.services.SettingsBuilderRequest;
42  import org.apache.maven.api.services.SettingsBuilderResult;
43  import org.apache.maven.api.services.Source;
44  import org.apache.maven.api.services.xml.SettingsXmlFactory;
45  import org.apache.maven.api.services.xml.XmlReaderException;
46  import org.apache.maven.api.services.xml.XmlReaderRequest;
47  import org.apache.maven.api.settings.Activation;
48  import org.apache.maven.api.settings.Profile;
49  import org.apache.maven.api.settings.Repository;
50  import org.apache.maven.api.settings.RepositoryPolicy;
51  import org.apache.maven.api.settings.Server;
52  import org.apache.maven.api.settings.Settings;
53  import org.apache.maven.internal.impl.model.DefaultInterpolator;
54  import org.apache.maven.settings.v4.SettingsMerger;
55  import org.apache.maven.settings.v4.SettingsTransformer;
56  import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
57  
58  /**
59   * Builds the effective settings from a user settings file and/or a global settings file.
60   *
61   */
62  @Named
63  public class DefaultSettingsBuilder implements SettingsBuilder {
64  
65      private final DefaultSettingsValidator settingsValidator = new DefaultSettingsValidator();
66  
67      private final SettingsMerger settingsMerger = new SettingsMerger();
68  
69      private final SettingsXmlFactory settingsXmlFactory;
70  
71      private final Interpolator interpolator;
72  
73      private final SecDispatcher secDispatcher;
74  
75      /**
76       * This ctor is used in legacy components, and when in legacy, {@link SecDispatcher} is {@code null} and
77       * Maven3 exposes decryption with other means.
78       */
79      public DefaultSettingsBuilder() {
80          this(new DefaultSettingsXmlFactory(), new DefaultInterpolator(), null);
81      }
82  
83      /**
84       * In Maven4 the {@link SecDispatcher} is injected and build settings are fully decrypted as well.
85       */
86      @Inject
87      public DefaultSettingsBuilder(
88              SettingsXmlFactory settingsXmlFactory, Interpolator interpolator, SecDispatcher secDispatcher) {
89          this.settingsXmlFactory = settingsXmlFactory;
90          this.interpolator = interpolator;
91          this.secDispatcher = secDispatcher;
92      }
93  
94      @Override
95      public SettingsBuilderResult build(SettingsBuilderRequest request) throws SettingsBuilderException {
96          List<BuilderProblem> problems = new ArrayList<>();
97  
98          Source installationSource = request.getInstallationSettingsSource().orElse(null);
99          Settings installation = readSettings(installationSource, false, request, problems);
100 
101         Source projectSource = request.getProjectSettingsSource().orElse(null);
102         Settings project = readSettings(projectSource, true, request, problems);
103 
104         Source userSource = request.getUserSettingsSource().orElse(null);
105         Settings user = readSettings(userSource, false, request, problems);
106 
107         Settings effective =
108                 settingsMerger.merge(user, settingsMerger.merge(project, installation, false, null), false, null);
109 
110         // If no repository is defined in the user/global settings,
111         // it means that we have "old" settings (as those are new in 4.0)
112         // so add central to the computed settings for backward compatibility.
113         if (effective.getRepositories().isEmpty()
114                 && effective.getPluginRepositories().isEmpty()) {
115             Repository central = Repository.newBuilder()
116                     .id("central")
117                     .name("Central Repository")
118                     .url("https://repo.maven.apache.org/maven2")
119                     .snapshots(RepositoryPolicy.newBuilder().enabled(false).build())
120                     .build();
121             Repository centralWithNoUpdate = central.withReleases(
122                     RepositoryPolicy.newBuilder().updatePolicy("never").build());
123             effective = Settings.newBuilder(effective)
124                     .repositories(List.of(central))
125                     .pluginRepositories(List.of(centralWithNoUpdate))
126                     .build();
127         }
128 
129         // for the special case of a drive-relative Windows path, make sure it's absolute to save plugins from trouble
130         String localRepository = effective.getLocalRepository();
131         if (localRepository != null && !localRepository.isEmpty()) {
132             Path file = Paths.get(localRepository);
133             if (!file.isAbsolute() && file.toString().startsWith(File.separator)) {
134                 effective = effective.withLocalRepository(file.toAbsolutePath().toString());
135             }
136         }
137 
138         if (hasErrors(problems)) {
139             throw new SettingsBuilderException("Error building settings", problems);
140         }
141 
142         return new DefaultSettingsBuilderResult(effective, problems);
143     }
144 
145     private boolean hasErrors(List<BuilderProblem> problems) {
146         if (problems != null) {
147             for (BuilderProblem problem : problems) {
148                 if (BuilderProblem.Severity.ERROR.compareTo(problem.getSeverity()) >= 0) {
149                     return true;
150                 }
151             }
152         }
153 
154         return false;
155     }
156 
157     private Settings readSettings(
158             Source settingsSource,
159             boolean isProjectSettings,
160             SettingsBuilderRequest request,
161             List<BuilderProblem> problems) {
162         if (settingsSource == null) {
163             return Settings.newInstance();
164         }
165 
166         Settings settings;
167 
168         try {
169             try (InputStream is = settingsSource.openStream()) {
170                 settings = settingsXmlFactory.read(XmlReaderRequest.builder()
171                         .inputStream(is)
172                         .location(settingsSource.getLocation())
173                         .strict(true)
174                         .build());
175             } catch (XmlReaderException e) {
176                 try (InputStream is = settingsSource.openStream()) {
177                     settings = settingsXmlFactory.read(XmlReaderRequest.builder()
178                             .inputStream(is)
179                             .location(settingsSource.getLocation())
180                             .strict(false)
181                             .build());
182                     Location loc = e.getCause() instanceof XMLStreamException xe ? xe.getLocation() : null;
183                     problems.add(new DefaultBuilderProblem(
184                             settingsSource.getLocation(),
185                             loc != null ? loc.getLineNumber() : -1,
186                             loc != null ? loc.getColumnNumber() : -1,
187                             e,
188                             e.getMessage(),
189                             BuilderProblem.Severity.WARNING));
190                 }
191             }
192         } catch (XmlReaderException e) {
193             Location loc = e.getCause() instanceof XMLStreamException xe ? xe.getLocation() : null;
194             problems.add(new DefaultBuilderProblem(
195                     settingsSource.getLocation(),
196                     loc != null ? loc.getLineNumber() : -1,
197                     loc != null ? loc.getColumnNumber() : -1,
198                     e,
199                     "Non-parseable settings " + settingsSource.getLocation() + ": " + e.getMessage(),
200                     BuilderProblem.Severity.FATAL));
201             return Settings.newInstance();
202         } catch (IOException e) {
203             problems.add(new DefaultBuilderProblem(
204                     settingsSource.getLocation(),
205                     -1,
206                     -1,
207                     e,
208                     "Non-readable settings " + settingsSource.getLocation() + ": " + e.getMessage(),
209                     BuilderProblem.Severity.FATAL));
210             return Settings.newInstance();
211         }
212 
213         settings = interpolate(settings, request, problems);
214         settings = decrypt(settingsSource, settings, request, problems);
215 
216         settingsValidator.validate(settings, isProjectSettings, problems);
217 
218         if (isProjectSettings) {
219             settings = Settings.newBuilder(settings, true)
220                     .localRepository(null)
221                     .interactiveMode(false)
222                     .offline(false)
223                     .proxies(List.of())
224                     .usePluginRegistry(false)
225                     .servers(settings.getServers().stream()
226                             .map(s -> Server.newBuilder(s, true)
227                                     .username(null)
228                                     .passphrase(null)
229                                     .privateKey(null)
230                                     .password(null)
231                                     .filePermissions(null)
232                                     .directoryPermissions(null)
233                                     .build())
234                             .toList())
235                     .build();
236         }
237 
238         return settings;
239     }
240 
241     private Settings interpolate(Settings settings, SettingsBuilderRequest request, List<BuilderProblem> problems) {
242         Function<String, String> src;
243         if (request.getInterpolationSource().isPresent()) {
244             src = request.getInterpolationSource().get();
245         } else {
246             Map<String, String> userProperties = request.getSession().getUserProperties();
247             Map<String, String> systemProperties = request.getSession().getSystemProperties();
248             src = Interpolator.chain(userProperties::get, systemProperties::get);
249         }
250         return new DefSettingsTransformer(value -> value != null ? interpolator.interpolate(value, src) : null)
251                 .visit(settings);
252     }
253 
254     static class DefSettingsTransformer extends SettingsTransformer {
255         DefSettingsTransformer(Function<String, String> transformer) {
256             super(transformer);
257         }
258 
259         @Override
260         protected Activation.Builder transformActivation_Condition(
261                 Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
262             // do not interpolate the condition activation
263             return builder;
264         }
265     }
266 
267     private Settings decrypt(
268             Source settingsSource, Settings settings, SettingsBuilderRequest request, List<BuilderProblem> problems) {
269         if (secDispatcher == null) {
270             return settings;
271         }
272         Function<String, String> decryptFunction = str -> {
273             if (secDispatcher.isAnyEncryptedString(str)) {
274                 if (secDispatcher.isLegacyEncryptedString(str)) {
275                     // add a problem
276                     problems.add(new DefaultBuilderProblem(
277                             settingsSource.getLocation(),
278                             -1,
279                             -1,
280                             null,
281                             "Pre-Maven 4 legacy encrypted password detected "
282                                     + " - configure password encryption with the help of mvnenc to be compatible with Maven 4.",
283                             BuilderProblem.Severity.WARNING));
284                 }
285                 try {
286                     return secDispatcher.decrypt(str);
287                 } catch (Exception e) {
288                     problems.add(new DefaultBuilderProblem(
289                             settingsSource.getLocation(),
290                             -1,
291                             -1,
292                             e,
293                             "Could not decrypt password (fix the corrupted password or remove it, if unused) " + str,
294                             BuilderProblem.Severity.ERROR));
295                 }
296             }
297             return str;
298         };
299         return new SettingsTransformer(decryptFunction).visit(settings);
300     }
301 
302     @Override
303     public List<BuilderProblem> validate(Settings settings, boolean isProjectSettings) {
304         ArrayList<BuilderProblem> problems = new ArrayList<>();
305         settingsValidator.validate(settings, isProjectSettings, problems);
306         return problems;
307     }
308 
309     @Override
310     public Profile convert(org.apache.maven.api.model.Profile profile) {
311         return SettingsUtilsV4.convertToSettingsProfile(profile);
312     }
313 
314     @Override
315     public org.apache.maven.api.model.Profile convert(Profile profile) {
316         return SettingsUtilsV4.convertFromSettingsProfile(profile);
317     }
318 
319     /**
320      * Collects the output of the settings builder.
321      *
322      */
323     static class DefaultSettingsBuilderResult implements SettingsBuilderResult {
324 
325         private final Settings effectiveSettings;
326 
327         private final List<BuilderProblem> problems;
328 
329         DefaultSettingsBuilderResult(Settings effectiveSettings, List<BuilderProblem> problems) {
330             this.effectiveSettings = effectiveSettings;
331             this.problems = (problems != null) ? problems : new ArrayList<>();
332         }
333 
334         @Override
335         public Settings getEffectiveSettings() {
336             return effectiveSettings;
337         }
338 
339         @Override
340         public List<BuilderProblem> getProblems() {
341             return problems;
342         }
343     }
344 }