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, true);
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             return MessageUtils.reader.readLine(message != null ? message + ": " : null, password ? '*' : null);
197         } catch (Exception e) {
198             throw new IOException("Unable to prompt user", e);
199         }
200     }
201 
202     class JlineMessageBuilder implements MessageBuilder {
203 
204         final AttributedStringBuilder builder;
205 
206         JlineMessageBuilder() {
207             builder = new AttributedStringBuilder();
208         }
209 
210         JlineMessageBuilder(int size) {
211             builder = new AttributedStringBuilder(size);
212         }
213 
214         @Override
215         public MessageBuilder style(String style) {
216             if (MessageUtils.isColorEnabled()) {
217                 builder.style(resolver.resolve(style));
218             }
219             return this;
220         }
221 
222         @Override
223         public MessageBuilder resetStyle() {
224             builder.style(AttributedStyle.DEFAULT);
225             return this;
226         }
227 
228         @Override
229         public MessageBuilder append(CharSequence cs) {
230             builder.append(cs);
231             return this;
232         }
233 
234         @Override
235         public MessageBuilder append(CharSequence cs, int start, int end) {
236             builder.append(cs, start, end);
237             return this;
238         }
239 
240         @Override
241         public MessageBuilder append(char c) {
242             builder.append(c);
243             return this;
244         }
245 
246         @Override
247         public MessageBuilder setLength(int length) {
248             builder.setLength(length);
249             return this;
250         }
251 
252         @Override
253         public String build() {
254             return builder.toAnsi(MessageUtils.terminal);
255         }
256 
257         @Override
258         public String toString() {
259             return build();
260         }
261     }
262 
263     static class MavenStyleResolver extends StyleResolver {
264 
265         private final Map<String, AttributedStyle> styles = new ConcurrentHashMap<>();
266 
267         MavenStyleResolver() {
268             super(s -> System.getProperty("style." + s));
269         }
270 
271         @Override
272         public AttributedStyle resolve(String spec) {
273             return styles.computeIfAbsent(spec, this::doResolve);
274         }
275 
276         @Override
277         public AttributedStyle resolve(String spec, String defaultSpec) {
278             return resolve(defaultSpec != null ? spec + ":-" + defaultSpec : spec);
279         }
280 
281         private AttributedStyle doResolve(String spec) {
282             String def = null;
283             int i = spec.indexOf(":-");
284             if (i != -1) {
285                 String[] parts = spec.split(":-");
286                 spec = parts[0].trim();
287                 def = parts[1].trim();
288             }
289             return super.resolve(spec, def);
290         }
291     }
292 }