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.cling.invoker.mvnenc.goals;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Objects;
28  
29  import org.apache.maven.api.cli.mvnenc.EncryptOptions;
30  import org.apache.maven.api.services.MessageBuilderFactory;
31  import org.apache.maven.cling.invoker.mvnenc.EncryptContext;
32  import org.codehaus.plexus.components.secdispatcher.DispatcherMeta;
33  import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
34  import org.codehaus.plexus.components.secdispatcher.model.Config;
35  import org.codehaus.plexus.components.secdispatcher.model.ConfigProperty;
36  import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity;
37  import org.jline.consoleui.elements.ConfirmChoice;
38  import org.jline.consoleui.prompt.ConfirmResult;
39  import org.jline.consoleui.prompt.ConsolePrompt;
40  import org.jline.consoleui.prompt.PromptResultItemIF;
41  import org.jline.consoleui.prompt.builder.ListPromptBuilder;
42  import org.jline.consoleui.prompt.builder.PromptBuilder;
43  import org.jline.reader.Candidate;
44  import org.jline.reader.Completer;
45  import org.jline.reader.LineReader;
46  import org.jline.reader.ParsedLine;
47  import org.jline.utils.Colors;
48  import org.jline.utils.OSUtils;
49  
50  import static org.apache.maven.cling.invoker.mvnenc.EncryptInvoker.BAD_OPERATION;
51  import static org.apache.maven.cling.invoker.mvnenc.EncryptInvoker.OK;
52  
53  /**
54   * The "init" goal.
55   */
56  @Singleton
57  @Named("init")
58  public class Init extends InteractiveGoalSupport {
59      private static final String NONE = "__none__";
60  
61      @Inject
62      public Init(MessageBuilderFactory messageBuilderFactory, SecDispatcher secDispatcher) {
63          super(messageBuilderFactory, secDispatcher);
64      }
65  
66      @Override
67      public int doExecute(EncryptContext context) throws Exception {
68          EncryptOptions options = (EncryptOptions) context.invokerRequest.options();
69          boolean force = options.force().orElse(false);
70          boolean yes = options.yes().orElse(false);
71  
72          if (configExists() && !force) {
73              context.logger.error(messageBuilderFactory
74                      .builder()
75                      .error("Error: configuration exist. Use --force if you want to reset existing configuration.")
76                      .build());
77              return BAD_OPERATION;
78          }
79  
80          context.addInHeader(context.style.italic().bold().foreground(Colors.rgbColor("yellow")), "goal: init");
81          context.addInHeader("");
82  
83          ConsolePrompt.UiConfig promptConfig;
84          if (OSUtils.IS_WINDOWS) {
85              promptConfig = new ConsolePrompt.UiConfig(">", "( )", "(x)", "( )");
86          } else {
87              promptConfig = new ConsolePrompt.UiConfig("❯", "◯ ", "◉ ", "◯ ");
88          }
89          promptConfig.setCancellableFirstPrompt(true);
90          ConsolePrompt prompt = new ConsolePrompt(context.reader, context.terminal, promptConfig);
91  
92          SettingsSecurity config = secDispatcher.readConfiguration(true);
93  
94          // reset config
95          config.setDefaultDispatcher(null);
96          config.getConfigurations().clear();
97  
98          Map<String, PromptResultItemIF> result = prompt.prompt(
99                  context.header, dispatcherPrompt(prompt.getPromptBuilder()).build());
100         if (result == null) {
101             throw new InterruptedException();
102         }
103         if (NONE.equals(result.get("defaultDispatcher").getResult())) {
104             context.terminal
105                     .writer()
106                     .println(messageBuilderFactory
107                             .builder()
108                             .warning(
109                                     "Maven4 SecDispatcher disabled; Maven3 fallback may still work, use `mvnenc diag` to check")
110                             .build());
111             secDispatcher.writeConfiguration(config);
112             return OK;
113         }
114         config.setDefaultDispatcher(result.get("defaultDispatcher").getResult());
115 
116         DispatcherMeta meta = secDispatcher.availableDispatchers().stream()
117                 .filter(d -> Objects.equals(config.getDefaultDispatcher(), d.name()))
118                 .findFirst()
119                 .orElseThrow();
120         if (!meta.fields().isEmpty()) {
121             result = prompt.prompt(
122                     context.header,
123                     configureDispatcher(context, meta, prompt.getPromptBuilder())
124                             .build());
125             if (result == null) {
126                 throw new InterruptedException();
127             }
128 
129             List<Map.Entry<String, PromptResultItemIF>> editables = result.entrySet().stream()
130                     .filter(e -> e.getValue().getResult().contains("$"))
131                     .toList();
132             if (!editables.isEmpty()) {
133                 context.addInHeader("");
134                 context.addInHeader("Please customize the editable value:");
135                 Map<String, PromptResultItemIF> editMap;
136                 for (Map.Entry<String, PromptResultItemIF> editable : editables) {
137                     String template = editable.getValue().getResult();
138                     String prefix = template.substring(0, template.indexOf("$"));
139                     editMap = prompt.prompt(
140                             context.header,
141                             prompt.getPromptBuilder()
142                                     .createInputPrompt()
143                                     .name("edit")
144                                     .message(template)
145                                     .addCompleter(new Completer() {
146                                         @Override
147                                         public void complete(
148                                                 LineReader reader, ParsedLine line, List<Candidate> candidates) {
149                                             if (!line.line().startsWith(prefix)) {
150                                                 candidates.add(
151                                                         new Candidate(prefix, prefix, null, null, null, null, false));
152                                             }
153                                         }
154                                     })
155                                     .addPrompt()
156                                     .build());
157                     if (editMap == null) {
158                         throw new InterruptedException();
159                     }
160                     result.put(editable.getKey(), editMap.get("edit"));
161                 }
162             }
163 
164             Config dispatcherConfig = new Config();
165             dispatcherConfig.setName(meta.name());
166             for (DispatcherMeta.Field field : meta.fields()) {
167                 ConfigProperty property = new ConfigProperty();
168                 property.setName(field.getKey());
169                 property.setValue(result.get(field.getKey()).getResult());
170                 dispatcherConfig.addProperty(property);
171             }
172             if (!dispatcherConfig.getProperties().isEmpty()) {
173                 config.addConfiguration(dispatcherConfig);
174             }
175         }
176 
177         if (yes) {
178             secDispatcher.writeConfiguration(config);
179         } else {
180             context.addInHeader("");
181             context.addInHeader("Values set:");
182             context.addInHeader("defaultDispatcher=" + config.getDefaultDispatcher());
183             for (Config c : config.getConfigurations()) {
184                 context.addInHeader("  dispatcherName=" + c.getName());
185                 for (ConfigProperty cp : c.getProperties()) {
186                     context.addInHeader("    " + cp.getName() + "=" + cp.getValue());
187                 }
188             }
189 
190             result = prompt.prompt(
191                     context.header, confirmPrompt(prompt.getPromptBuilder()).build());
192             ConfirmResult confirm = (ConfirmResult) result.get("confirm");
193             if (confirm.getConfirmed() == ConfirmChoice.ConfirmationValue.YES) {
194                 context.terminal
195                         .writer()
196                         .println(messageBuilderFactory
197                                 .builder()
198                                 .info("Writing out the configuration...")
199                                 .build());
200                 secDispatcher.writeConfiguration(config);
201             } else {
202                 context.terminal
203                         .writer()
204                         .println(messageBuilderFactory
205                                 .builder()
206                                 .warning("Values not accepted; not saving configuration.")
207                                 .build());
208                 return BAD_OPERATION;
209             }
210         }
211 
212         return OK;
213     }
214 
215     protected PromptBuilder confirmPrompt(PromptBuilder promptBuilder) {
216         promptBuilder
217                 .createConfirmPromp()
218                 .name("confirm")
219                 .message("Are values above correct?")
220                 .defaultValue(ConfirmChoice.ConfirmationValue.YES)
221                 .addPrompt();
222         return promptBuilder;
223     }
224 
225     protected PromptBuilder dispatcherPrompt(PromptBuilder promptBuilder) {
226         ListPromptBuilder listPromptBuilder = promptBuilder
227                 .createListPrompt()
228                 .name("defaultDispatcher")
229                 .message("Which dispatcher you want to use as default?");
230         listPromptBuilder
231                 .newItem()
232                 .name(NONE)
233                 .text("None (disable MavenSecDispatcher)")
234                 .add();
235         for (DispatcherMeta meta : secDispatcher.availableDispatchers()) {
236             if (!meta.isHidden()) {
237                 listPromptBuilder
238                         .newItem()
239                         .name(meta.name())
240                         .text(meta.displayName())
241                         .add();
242             }
243         }
244         listPromptBuilder.addPrompt();
245         return promptBuilder;
246     }
247 
248     private PromptBuilder configureDispatcher(
249             EncryptContext context, DispatcherMeta dispatcherMeta, PromptBuilder promptBuilder) throws Exception {
250         context.addInHeader(
251                 context.style.italic().bold().foreground(Colors.rgbColor("yellow")),
252                 "Configure " + dispatcherMeta.displayName());
253         context.addInHeader("");
254 
255         for (DispatcherMeta.Field field : dispatcherMeta.fields()) {
256             String fieldKey = field.getKey();
257             String fieldDescription = "Configure " + fieldKey + ": " + field.getDescription();
258             if (field.getOptions().isPresent()) {
259                 // list options
260                 ListPromptBuilder listPromptBuilder =
261                         promptBuilder.createListPrompt().name(fieldKey).message(fieldDescription);
262                 for (DispatcherMeta.Field option : field.getOptions().get()) {
263                     listPromptBuilder
264                             .newItem()
265                             .name(
266                                     option.getDefaultValue().isPresent()
267                                             ? option.getDefaultValue().get()
268                                             : option.getKey())
269                             .text(option.getDescription())
270                             .add();
271                 }
272                 listPromptBuilder.addPrompt();
273             } else if (field.getDefaultValue().isPresent()) {
274                 // input w/ def value
275                 promptBuilder
276                         .createInputPrompt()
277                         .name(fieldKey)
278                         .message(fieldDescription)
279                         .defaultValue(field.getDefaultValue().get())
280                         .addPrompt();
281             } else {
282                 // ? plain input?
283                 promptBuilder
284                         .createInputPrompt()
285                         .name(fieldKey)
286                         .message(fieldDescription)
287                         .addPrompt();
288             }
289         }
290         return promptBuilder;
291     }
292 }