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