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