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