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.cli.jline;
20  
21  import javax.annotation.Priority;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.IOException;
26  import java.util.ArrayList;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.concurrent.ConcurrentHashMap;
31  
32  import org.apache.maven.api.annotations.Experimental;
33  import org.apache.maven.api.services.MessageBuilder;
34  import org.apache.maven.api.services.MessageBuilderFactory;
35  import org.codehaus.plexus.components.interactivity.InputHandler;
36  import org.codehaus.plexus.components.interactivity.OutputHandler;
37  import org.codehaus.plexus.components.interactivity.Prompter;
38  import org.codehaus.plexus.components.interactivity.PrompterException;
39  import org.jline.utils.AttributedStringBuilder;
40  import org.jline.utils.AttributedStyle;
41  import org.jline.utils.StyleResolver;
42  
43  import static org.jline.utils.AttributedStyle.DEFAULT;
44  
45  @Experimental
46  @Named
47  @Singleton
48  @Priority(10)
49  public class JLineMessageBuilderFactory implements MessageBuilderFactory, Prompter, InputHandler, OutputHandler {
50  
51      private final StyleResolver resolver;
52  
53      public JLineMessageBuilderFactory() {
54          this.resolver = new MavenStyleResolver();
55      }
56  
57      @Override
58      public boolean isColorEnabled() {
59          return false;
60      }
61  
62      @Override
63      public int getTerminalWidth() {
64          return MessageUtils.getTerminalWidth();
65      }
66  
67      @Override
68      public MessageBuilder builder() {
69          return new JlineMessageBuilder();
70      }
71  
72      @Override
73      public MessageBuilder builder(int size) {
74          return new JlineMessageBuilder(size);
75      }
76  
77      @Override
78      public String readLine() throws IOException {
79          return doPrompt(null, true);
80      }
81  
82      @Override
83      public String readPassword() throws IOException {
84          return doPrompt(null, true);
85      }
86  
87      @Override
88      public List<String> readMultipleLines() throws IOException {
89          List<String> lines = new ArrayList<>();
90          for (String line = this.readLine(); line != null && !line.isEmpty(); line = readLine()) {
91              lines.add(line);
92          }
93          return lines;
94      }
95  
96      @Override
97      public void write(String line) throws IOException {
98          doDisplay(line);
99      }
100 
101     @Override
102     public void writeLine(String line) throws IOException {
103         doDisplay(line + System.lineSeparator());
104     }
105 
106     @Override
107     public String prompt(String message) throws PrompterException {
108         return prompt(message, null, null);
109     }
110 
111     @Override
112     public String prompt(String message, String defaultReply) throws PrompterException {
113         return prompt(message, null, defaultReply);
114     }
115 
116     @Override
117     public String prompt(String message, List possibleValues) throws PrompterException {
118         return prompt(message, possibleValues, null);
119     }
120 
121     @Override
122     public String prompt(String message, List possibleValues, String defaultReply) throws PrompterException {
123         return doPrompt(message, possibleValues, defaultReply, false);
124     }
125 
126     @Override
127     public String promptForPassword(String message) throws PrompterException {
128         return doPrompt(message, null, null, true);
129     }
130 
131     @Override
132     public void showMessage(String message) throws PrompterException {
133         try {
134             doDisplay(message);
135         } catch (IOException e) {
136             throw new PrompterException("Failed to present prompt", e);
137         }
138     }
139 
140     String doPrompt(String message, List<Object> possibleValues, String defaultReply, boolean password)
141             throws PrompterException {
142         String formattedMessage = formatMessage(message, possibleValues, defaultReply);
143         String line;
144         do {
145             try {
146                 line = doPrompt(formattedMessage, password);
147                 if (line == null && defaultReply == null) {
148                     throw new IOException("EOF");
149                 }
150             } catch (IOException e) {
151                 throw new PrompterException("Failed to prompt user", e);
152             }
153             if (line == null || line.isEmpty()) {
154                 line = defaultReply;
155             }
156             if (line != null && (possibleValues != null && !possibleValues.contains(line))) {
157                 try {
158                     doDisplay("Invalid selection.\n");
159                 } catch (IOException e) {
160                     throw new PrompterException("Failed to present feedback", e);
161                 }
162             }
163         } while (line == null || (possibleValues != null && !possibleValues.contains(line)));
164         return line;
165     }
166 
167     private String formatMessage(String message, List<Object> possibleValues, String defaultReply) {
168         StringBuilder formatted = new StringBuilder(message.length() * 2);
169         formatted.append(message);
170         if (possibleValues != null && !possibleValues.isEmpty()) {
171             formatted.append(" (");
172             for (Iterator<?> it = possibleValues.iterator(); it.hasNext(); ) {
173                 String possibleValue = String.valueOf(it.next());
174                 formatted.append(possibleValue);
175                 if (it.hasNext()) {
176                     formatted.append('/');
177                 }
178             }
179             formatted.append(')');
180         }
181         if (defaultReply != null) {
182             formatted.append(' ').append(defaultReply).append(": ");
183         }
184         return formatted.toString();
185     }
186 
187     private void doDisplay(String message) throws IOException {
188         try {
189             MessageUtils.terminal.writer().print(message);
190             MessageUtils.terminal.flush();
191         } catch (Exception e) {
192             throw new IOException("Unable to display message", e);
193         }
194     }
195 
196     private String doPrompt(String message, boolean password) throws IOException {
197         try {
198             return MessageUtils.reader.readLine(message != null ? message + ": " : null, password ? '*' : null);
199         } catch (Exception e) {
200             throw new IOException("Unable to prompt user", e);
201         }
202     }
203 
204     class JlineMessageBuilder implements MessageBuilder {
205 
206         final AttributedStringBuilder builder;
207 
208         JlineMessageBuilder() {
209             builder = new AttributedStringBuilder();
210         }
211 
212         JlineMessageBuilder(int size) {
213             builder = new AttributedStringBuilder(size);
214         }
215 
216         @Override
217         public MessageBuilder style(String style) {
218             if (MessageUtils.isColorEnabled()) {
219                 builder.style(resolver.resolve(style));
220             }
221             return this;
222         }
223 
224         @Override
225         public MessageBuilder resetStyle() {
226             builder.style(DEFAULT);
227             return this;
228         }
229 
230         @Override
231         public MessageBuilder append(CharSequence cs) {
232             builder.append(cs);
233             return this;
234         }
235 
236         @Override
237         public MessageBuilder append(CharSequence cs, int start, int end) {
238             builder.append(cs, start, end);
239             return this;
240         }
241 
242         @Override
243         public MessageBuilder append(char c) {
244             builder.append(c);
245             return this;
246         }
247 
248         @Override
249         public MessageBuilder setLength(int length) {
250             builder.setLength(length);
251             return this;
252         }
253 
254         @Override
255         public String build() {
256             return builder.toAnsi(MessageUtils.terminal);
257         }
258 
259         @Override
260         public String toString() {
261             return build();
262         }
263     }
264 
265     static class MavenStyleResolver extends StyleResolver {
266 
267         private final Map<String, AttributedStyle> styles = new ConcurrentHashMap<>();
268 
269         MavenStyleResolver() {
270             super(s -> System.getProperty("style." + s));
271         }
272 
273         @Override
274         public AttributedStyle resolve(String spec) {
275             return styles.computeIfAbsent(spec, this::doResolve);
276         }
277 
278         @Override
279         public AttributedStyle resolve(String spec, String defaultSpec) {
280             return resolve(defaultSpec != null ? spec + ":-" + defaultSpec : spec);
281         }
282 
283         private AttributedStyle doResolve(String spec) {
284             String def = null;
285             int i = spec.indexOf(":-");
286             if (i != -1) {
287                 String[] parts = spec.split(":-");
288                 spec = parts[0].trim();
289                 def = parts[1].trim();
290             }
291             return super.resolve(spec, def);
292         }
293     }
294 }