1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 }