1 /*
2  * Copyright (c) 2016, 2020, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package jdk.internal.jshell.tool;
27 
28 import java.util.*;
29 import java.util.Map.Entry;
30 import java.util.function.Consumer;
31 import java.util.function.Function;
32 import java.util.regex.Matcher;
33 import java.util.regex.Pattern;
34 
35 import jdk.internal.jshell.tool.JShellTool.CompletionProvider;
36 
37 import static java.util.stream.Collectors.*;
38 import static jdk.internal.jshell.tool.ContinuousCompletionProvider.PERFECT_MATCHER;
39 import static jdk.internal.jshell.tool.JShellTool.EMPTY_COMPLETION_PROVIDER;
40 import static jdk.internal.jshell.tool.Selector.SelectorKind;
41 import static jdk.internal.jshell.tool.Selector.SelectorInstanceWithDoc;
42 import static jdk.internal.jshell.tool.Selector.SelectorBuilder;
43 import static jdk.internal.jshell.tool.Selector.FormatAction;
44 import static jdk.internal.jshell.tool.Selector.FormatCase;
45 import static jdk.internal.jshell.tool.Selector.FormatErrors;
46 import static jdk.internal.jshell.tool.Selector.FormatResolve;
47 import static jdk.internal.jshell.tool.Selector.FormatUnresolved;
48 import static jdk.internal.jshell.tool.Selector.FormatWhen;
49 
50 
51 /**
52  * Feedback customization support
53  *
54  * @author Robert Field
55  */
56 class Feedback {
57 
58     // Patern for substituted fields within a customized format string
59     private static final Pattern FIELD_PATTERN = Pattern.compile("\\{(.*?)\\}");
60 
61     // Internal field name for truncation length
62     private static final String TRUNCATION_FIELD = "<truncation>";
63 
64     // For encoding to Properties String
65     private static final String RECORD_SEPARATOR = "\u241E";
66 
67     // Selector for truncation of var value
68     private static final Selector VAR_VALUE_ADD_SELECTOR = new Selector(
69             FormatCase.VARVALUE,
70             FormatAction.ADDED,
71             FormatWhen.PRIMARY,
72             FormatResolve.OK,
73             FormatUnresolved.UNRESOLVED0,
74             FormatErrors.ERROR0);
75 
76     // Selector for typeKind record
77     private static final Selector RECORD_TYPEKIND_SELECTOR = new Selector(
78             EnumSet.of(FormatCase.RECORD),
79             EnumSet.noneOf(FormatAction.class),
80             EnumSet.noneOf(FormatWhen.class),
81             EnumSet.noneOf(FormatResolve.class),
82             EnumSet.noneOf(FormatUnresolved.class),
83             EnumSet.noneOf(FormatErrors.class));
84 
85     // Current mode -- initial value is placeholder during start-up
86     private Mode mode = new Mode("");
87 
88     // Retained current mode -- for checks
89     private Mode retainedCurrentMode = null;
90 
91     // Mapping of mode name to mode
92     private final Map<String, Mode> modeMap = new HashMap<>();
93 
94     // Mapping of mode names to encoded retained mode
95     private final Map<String, String> retainedMap = new HashMap<>();
96 
shouldDisplayCommandFluff()97     public boolean shouldDisplayCommandFluff() {
98         return mode.commandFluff;
99     }
100 
getPre()101     public String getPre() {
102         return mode.format("pre", Selector.ANY);
103     }
104 
getPost()105     public String getPost() {
106         return mode.format("post", Selector.ANY);
107     }
108 
getErrorPre()109     public String getErrorPre() {
110         return mode.format("errorpre", Selector.ANY);
111     }
112 
getErrorPost()113     public String getErrorPost() {
114         return mode.format("errorpost", Selector.ANY);
115     }
116 
format(FormatCase fc, FormatAction fa, FormatWhen fw, FormatResolve fr, FormatUnresolved fu, FormatErrors fe, String name, String type, String value, String unresolved, List<String> errorLines)117     public String format(FormatCase fc, FormatAction fa, FormatWhen fw,
118                          FormatResolve fr, FormatUnresolved fu, FormatErrors fe,
119                          String name, String type, String value, String unresolved, List<String> errorLines) {
120         return mode.format(fc, fa, fw, fr, fu, fe,
121                 name, type, value, unresolved, errorLines);
122     }
123 
format(String field, FormatCase fc, FormatAction fa, FormatWhen fw, FormatResolve fr, FormatUnresolved fu, FormatErrors fe, String name, String type, String value, String unresolved, List<String> errorLines)124     public String format(String field, FormatCase fc, FormatAction fa, FormatWhen fw,
125                          FormatResolve fr, FormatUnresolved fu, FormatErrors fe,
126                          String name, String type, String value, String unresolved, List<String> errorLines) {
127         return mode.format(field, fc, fa, fw, fr, fu, fe,
128                 name, type, value, unresolved, errorLines);
129     }
130 
truncateVarValue(String value)131     public String truncateVarValue(String value) {
132         return mode.truncateVarValue(value);
133     }
134 
getPrompt(String nextId)135     public String getPrompt(String nextId) {
136         return mode.getPrompt(nextId);
137     }
138 
getContinuationPrompt(String nextId)139     public String getContinuationPrompt(String nextId) {
140         return mode.getContinuationPrompt(nextId);
141     }
142 
setFeedback(MessageHandler messageHandler, ArgTokenizer at, Consumer<String> retainer)143     public boolean setFeedback(MessageHandler messageHandler, ArgTokenizer at, Consumer<String> retainer) {
144         return new Setter(messageHandler, at).setFeedback(retainer);
145     }
146 
setFormat(MessageHandler messageHandler, ArgTokenizer at)147     public boolean setFormat(MessageHandler messageHandler, ArgTokenizer at) {
148         return new Setter(messageHandler, at).setFormat();
149     }
150 
setTruncation(MessageHandler messageHandler, ArgTokenizer at)151     public boolean setTruncation(MessageHandler messageHandler, ArgTokenizer at) {
152         return new Setter(messageHandler, at).setTruncation();
153     }
154 
setMode(MessageHandler messageHandler, ArgTokenizer at, Consumer<String> retainer)155     public boolean setMode(MessageHandler messageHandler, ArgTokenizer at, Consumer<String> retainer) {
156         return new Setter(messageHandler, at).setMode(retainer);
157     }
158 
setPrompt(MessageHandler messageHandler, ArgTokenizer at)159     public boolean setPrompt(MessageHandler messageHandler, ArgTokenizer at) {
160         return new Setter(messageHandler, at).setPrompt();
161     }
162 
restoreEncodedModes(MessageHandler messageHandler, String encoded)163     public boolean restoreEncodedModes(MessageHandler messageHandler, String encoded) {
164         return new Setter(messageHandler, new ArgTokenizer("<init>", "")).restoreEncodedModes(encoded);
165     }
166 
markModesReadOnly()167     public void markModesReadOnly() {
168         modeMap.values().stream()
169                 .forEach(m -> m.readOnly = true);
170     }
171 
modeCompletions()172     JShellTool.CompletionProvider modeCompletions() {
173         return modeCompletions(EMPTY_COMPLETION_PROVIDER);
174     }
175 
modeCompletions(CompletionProvider successor)176     JShellTool.CompletionProvider modeCompletions(CompletionProvider successor) {
177         return new ContinuousCompletionProvider(
178                 () -> modeMap.keySet().stream()
179                         .collect(toMap(Function.identity(), m -> successor)),
180                 PERFECT_MATCHER);
181     }
182 
183     /**
184      * Holds all the context of a mode mode
185      */
186     private static class Mode {
187 
188         // Name of mode
189         final String name;
190 
191         // Display command verification/information
192         boolean commandFluff;
193 
194         // Setting (including format) by field
195         final Map<String, List<Setting>> byField;
196 
197         boolean readOnly = false;
198 
199         String prompt = "\n-> ";
200         String continuationPrompt = ">> ";
201 
202         static class Setting {
203 
204             final String format;
205             final Selector selector;
206 
Setting(String format, Selector selector)207             Setting(String format, Selector selector) {
208                 this.format = format;
209                 this.selector = selector;
210             }
211 
212             @Override
equals(Object o)213             public boolean equals(Object o) {
214                 if (o instanceof Setting) {
215                     Setting ing = (Setting) o;
216                     return format.equals(ing.format)
217                             && selector.equals(ing.selector);
218                 } else {
219                     return false;
220                 }
221             }
222 
223             @Override
hashCode()224             public int hashCode() {
225                 int hash = 7;
226                 hash = 67 * hash + Objects.hashCode(this.selector);
227                 hash = 67 * hash + Objects.hashCode(this.format);
228                 return hash;
229             }
230 
231             @Override
toString()232             public String toString() {
233                 return "Setting(" + format + "," + selector.toString() + ")";
234             }
235         }
236 
237         /**
238          * Set up an empty mode.
239          *
240          * @param name
241          */
Mode(String name)242         Mode(String name) {
243             this.name = name;
244             this.byField = new HashMap<>();
245             set("name", "%1$s", Selector.ALWAYS);
246             set("type", "%2$s", Selector.ALWAYS);
247             set("value", "%3$s", Selector.ALWAYS);
248             set("unresolved", "%4$s", Selector.ALWAYS);
249             set("errors", "%5$s", Selector.ALWAYS);
250             set("err", "%6$s", Selector.ALWAYS);
251 
252             set("errorline", "    {err}%n", Selector.ALWAYS);
253 
254             set("pre", "|  ", Selector.ALWAYS);
255             set("post", "%n", Selector.ALWAYS);
256             set("errorpre", "|  ", Selector.ALWAYS);
257             set("errorpost", "%n", Selector.ALWAYS);
258         }
259 
Mode(String name, boolean commandFluff, String prompt, String continuationPrompt)260         private Mode(String name, boolean commandFluff, String prompt, String continuationPrompt) {
261             this.name = name;
262             this.commandFluff = commandFluff;
263             this.prompt = prompt;
264             this.continuationPrompt = continuationPrompt;
265             this.byField = new HashMap<>();
266         }
267 
268         /**
269          * Set up a copied mode.
270          *
271          * @param name
272          * @param m    Mode to copy, or null for no fresh
273          */
Mode(String name, Mode m)274         Mode(String name, Mode m) {
275             this(name, m.commandFluff, m.prompt, m.continuationPrompt);
276             m.byField.forEach((fieldName, settingList) ->
277                     settingList.forEach(setting -> set(fieldName, setting.format, setting.selector)));
278 
279         }
280 
281         @Override
equals(Object o)282         public boolean equals(Object o) {
283             if (o instanceof Mode) {
284                 Mode m = (Mode) o;
285                 return name.equals((m.name))
286                         && commandFluff == m.commandFluff
287                         && prompt.equals((m.prompt))
288                         && continuationPrompt.equals((m.continuationPrompt))
289                         && byField.equals((m.byField));
290             } else {
291                 return false;
292             }
293         }
294 
295         @Override
hashCode()296         public int hashCode() {
297             return Objects.hashCode(name);
298         }
299 
300         /**
301          * Set if this mode displays informative/confirmational messages on
302          * commands.
303          *
304          * @param fluff the value to set
305          */
setCommandFluff(boolean fluff)306         void setCommandFluff(boolean fluff) {
307             commandFluff = fluff;
308         }
309 
310         /**
311          * Encodes the mode into a String so it can be saved in Preferences.
312          *
313          * @return the string representation
314          */
encode()315         String encode() {
316             List<String> el = new ArrayList<>();
317             el.add(name);
318             el.add(String.valueOf(commandFluff));
319             el.add(prompt);
320             el.add(continuationPrompt);
321             for (Entry<String, List<Setting>> es : byField.entrySet()) {
322                 el.add(es.getKey());
323                 el.add("(");
324                 for (Setting ing : es.getValue()) {
325                     el.add(ing.selector.toString());
326                     el.add(ing.format);
327                 }
328                 el.add(")");
329             }
330             el.add("***");
331             return String.join(RECORD_SEPARATOR, el);
332         }
333 
add(String field, Setting ing)334         private void add(String field, Setting ing) {
335             List<Setting> settings = byField.get(field);
336             if (settings == null) {
337                 settings = new ArrayList<>();
338                 byField.put(field, settings);
339             } else {
340                 // remove completely obscured settings.
341                 // transformation of partially obscured would be confusing to user and complex
342                 Selector addedSelector = ing.selector;
343                 settings.removeIf(t -> t.selector.includedIn(addedSelector));
344             }
345             settings.add(ing);
346         }
347 
set(String field, String format, Selector selector)348         void set(String field, String format, Selector selector) {
349             add(field, new Setting(format, selector));
350         }
351 
352         /**
353          * Lookup format Replace fields with context specific formats.
354          *
355          * @return format string
356          */
format(String field, Selector selector)357         String format(String field, Selector selector) {
358             List<Setting> settings = byField.get(field);
359             if (settings == null) {
360                 return ""; //TODO error?
361             }
362             String format = null;
363             // Iterate backward, as most recent setting that covers the case is used
364             for (int i = settings.size() - 1; i >= 0; --i) {
365                 Setting ing = settings.get(i);
366                 if (ing.selector.covers(selector)) {
367                     format = ing.format;
368                     break;
369                 }
370             }
371             if (format == null || format.isEmpty()) {
372                 return "";
373             }
374             Matcher m = FIELD_PATTERN.matcher(format);
375             StringBuffer sb = new StringBuffer(format.length());
376             while (m.find()) {
377                 String fieldName = m.group(1);
378                 String sub = format(fieldName, selector);
379                 m.appendReplacement(sb, Matcher.quoteReplacement(sub));
380             }
381             m.appendTail(sb);
382             return sb.toString();
383         }
384 
truncateVarValue(String value)385         String truncateVarValue(String value) {
386             return truncateValue(value, VAR_VALUE_ADD_SELECTOR);
387         }
388 
truncateValue(String value, Selector selector)389         String truncateValue(String value, Selector selector) {
390             if (value==null) {
391                 return "";
392             } else {
393                 // Retrieve the truncation length
394                 String truncField = format(TRUNCATION_FIELD, selector);
395                 if (truncField.isEmpty()) {
396                     // No truncation set, use whole value
397                     return value;
398                 } else {
399                     // Convert truncation length to int
400                     // this is safe since it has been tested before it is set
401                     int trunc = Integer.parseUnsignedInt(truncField);
402                     int len = value.length();
403                     if (len > trunc) {
404                         if (trunc <= 13) {
405                             // Very short truncations have no room for "..."
406                             return value.substring(0, trunc);
407                         } else {
408                             // Normal truncation, make total length equal truncation length
409                             int endLen = trunc / 3;
410                             int startLen = trunc - 5 - endLen;
411                             return value.substring(0, startLen) + " ... " + value.substring(len -endLen);
412                         }
413                     } else {
414                         // Within truncation length, use whole value
415                         return value;
416                     }
417                 }
418             }
419         }
420 
421         // Compute the display output given full context and values
format(FormatCase fc, FormatAction fa, FormatWhen fw, FormatResolve fr, FormatUnresolved fu, FormatErrors fe, String name, String type, String value, String unresolved, List<String> errorLines)422         String format(FormatCase fc, FormatAction fa, FormatWhen fw,
423                       FormatResolve fr, FormatUnresolved fu, FormatErrors fe,
424                       String name, String type, String value, String unresolved, List<String> errorLines) {
425             return format("display", fc, fa, fw, fr, fu, fe,
426                 name, type, value, unresolved, errorLines);
427         }
428 
429         // Compute the display output given full context and values
format(String field, FormatCase fc, FormatAction fa, FormatWhen fw, FormatResolve fr, FormatUnresolved fu, FormatErrors fe, String name, String type, String value, String unresolved, List<String> errorLines)430         String format(String field, FormatCase fc, FormatAction fa, FormatWhen fw,
431                       FormatResolve fr, FormatUnresolved fu, FormatErrors fe,
432                       String name, String type, String value, String unresolved, List<String> errorLines) {
433             // Convert the context into a bit representation used as selectors for store field formats
434             Selector selector  = new Selector(fc, fa, fw, fr, fu, fe);
435             String fname = name==null? "" : name;
436             String ftype = type==null? "" : type;
437             // Compute the representation of value
438             String fvalue = truncateValue(value, selector);
439             String funresolved = unresolved==null? "" : unresolved;
440             String errors = errorLines.stream()
441                     .map(el -> String.format(
442                             format("errorline", selector),
443                             fname, ftype, fvalue, funresolved, "*cannot-use-errors-here*", el))
444                     .collect(joining());
445             return String.format(
446                     format(field, selector),
447                     fname, ftype, fvalue, funresolved, errors, "*cannot-use-err-here*");
448         }
449 
setPrompts(String prompt, String continuationPrompt)450         void setPrompts(String prompt, String continuationPrompt) {
451             this.prompt = prompt;
452             this.continuationPrompt = continuationPrompt;
453         }
454 
getPrompt(String nextId)455         String getPrompt(String nextId) {
456             return String.format(prompt, nextId);
457         }
458 
getContinuationPrompt(String nextId)459         String getContinuationPrompt(String nextId) {
460             return String.format(continuationPrompt, nextId);
461         }
462     }
463 
464     // Class used to set custom eval output formats
465     // For both /set format  -- Parse arguments, setting custom format, or printing error
466     private class Setter {
467 
468         private final ArgTokenizer at;
469         private final MessageHandler messageHandler;
470         boolean valid = true;
471 
Setter(MessageHandler messageHandler, ArgTokenizer at)472         Setter(MessageHandler messageHandler, ArgTokenizer at) {
473             this.messageHandler = messageHandler;
474             this.at = at;
475             at.allowedOptions("-retain");
476         }
477 
fluff(String format, Object... args)478         void fluff(String format, Object... args) {
479             messageHandler.fluff(format, args);
480         }
481 
hard(String format, Object... args)482         void hard(String format, Object... args) {
483             messageHandler.hard(format, args);
484         }
485 
fluffmsg(String messageKey, Object... args)486         void fluffmsg(String messageKey, Object... args) {
487             messageHandler.fluffmsg(messageKey, args);
488         }
489 
hardmsg(String messageKey, Object... args)490         void hardmsg(String messageKey, Object... args) {
491             messageHandler.hardmsg(messageKey, args);
492         }
493 
showFluff()494         boolean showFluff() {
495             return messageHandler.showFluff();
496         }
497 
errorat(String messageKey, Object... args)498         void errorat(String messageKey, Object... args) {
499             if (!valid) {
500                 // no spew of errors
501                 return;
502             }
503             valid = false;
504             Object[] a2 = Arrays.copyOf(args, args.length + 2);
505             a2[args.length] = at.whole();
506             messageHandler.errormsg(messageKey, a2);
507         }
508 
509         // Show format settings -- in a predictable order, for testing...
showFormatSettings(Mode sm, String f)510         void showFormatSettings(Mode sm, String f) {
511             if (sm == null) {
512                 modeMap.entrySet().stream()
513                         .sorted((es1, es2) -> es1.getKey().compareTo(es2.getKey()))
514                         .forEach(m -> showFormatSettings(m.getValue(), f));
515             } else {
516                 sm.byField.entrySet().stream()
517                         .filter(ec -> (f == null)
518                             ? !ec.getKey().equals(TRUNCATION_FIELD)
519                             : ec.getKey().equals(f))
520                         .sorted((ec1, ec2) -> ec1.getKey().compareTo(ec2.getKey()))
521                         .forEach(ec -> {
522                             ec.getValue().forEach(s -> {
523                                 hard("/set format %s %s %s %s",
524                                         sm.name, ec.getKey(), toStringLiteral(s.format),
525                                         s.selector.toString());
526 
527                             });
528                         });
529             }
530         }
531 
showTruncationSettings(Mode sm)532         void showTruncationSettings(Mode sm) {
533             if (sm == null) {
534                 modeMap.values().forEach(this::showTruncationSettings);
535             } else {
536                 List<Mode.Setting> trunc = sm.byField.get(TRUNCATION_FIELD);
537                 if (trunc != null) {
538                     trunc.forEach(s -> {
539                         hard("/set truncation %s %s %s",
540                                 sm.name, s.format,
541                                 s.selector.toString());
542                     });
543                 }
544             }
545         }
546 
showPromptSettings(Mode sm)547         void showPromptSettings(Mode sm) {
548             if (sm == null) {
549                 modeMap.values().forEach(this::showPromptSettings);
550             } else {
551                 hard("/set prompt %s %s %s",
552                         sm.name,
553                         toStringLiteral(sm.prompt),
554                         toStringLiteral(sm.continuationPrompt));
555             }
556         }
557 
showModeSettings(String umode, String msg)558         void showModeSettings(String umode, String msg) {
559             if (umode == null) {
560                 modeMap.values().forEach(this::showModeSettings);
561             } else {
562                 Mode m;
563                 String retained = retainedMap.get(umode);
564                 if (retained == null) {
565                     m = searchForMode(umode, msg);
566                     if (m == null) {
567                         return;
568                     }
569                     umode = m.name;
570                     retained = retainedMap.get(umode);
571                 } else {
572                     m = modeMap.get(umode);
573                 }
574                 if (retained != null) {
575                     Mode rm = buildMode(encodedModeIterator(retained));
576                     showModeSettings(rm);
577                     hard("/set mode -retain %s", umode);
578                     if (m != null && !m.equals(rm)) {
579                         hard("");
580                         showModeSettings(m);
581                     }
582                 } else {
583                     showModeSettings(m);
584                 }
585             }
586         }
587 
showModeSettings(Mode sm)588         void showModeSettings(Mode sm) {
589             hard("/set mode %s %s",
590                     sm.name, sm.commandFluff ? "-command" : "-quiet");
591             showPromptSettings(sm);
592             showFormatSettings(sm, null);
593             showTruncationSettings(sm);
594         }
595 
showFeedbackSetting()596         void showFeedbackSetting() {
597             if (retainedCurrentMode != null) {
598                 hard("/set feedback -retain %s", retainedCurrentMode.name);
599             }
600             if (mode != retainedCurrentMode) {
601                 hard("/set feedback %s", mode.name);
602             }
603         }
604 
605         // For /set prompt <mode> "<prompt>" "<continuation-prompt>"
setPrompt()606         boolean setPrompt() {
607             Mode m = nextMode();
608             String prompt = nextFormat();
609             String continuationPrompt = nextFormat();
610             checkOptionsAndRemainingInput();
611             if (valid && prompt == null) {
612                 showPromptSettings(m);
613                 return valid;
614             }
615             if (valid && m.readOnly) {
616                 errorat("jshell.err.not.valid.with.predefined.mode", m.name);
617             } else if (continuationPrompt == null) {
618                 errorat("jshell.err.continuation.prompt.required");
619             }
620             if (valid) {
621                 m.setPrompts(prompt, continuationPrompt);
622             } else {
623                 fluffmsg("jshell.msg.see", "/help /set prompt");
624             }
625             return valid;
626         }
627 
628         /**
629          * Set mode. Create, changed, or delete a feedback mode. For @{code /set
630          * mode <mode> [<old-mode>] [-command|-quiet|-delete|-retain]}.
631          *
632          * @return true if successful
633          */
setMode(Consumer<String> retainer)634         boolean setMode(Consumer<String> retainer) {
635             class SetMode {
636 
637                 final String umode;
638                 final String omode;
639                 final boolean commandOption;
640                 final boolean quietOption;
641                 final boolean deleteOption;
642                 final boolean retainOption;
643 
644                 SetMode() {
645                     at.allowedOptions("-command", "-quiet", "-delete", "-retain");
646                     umode = nextModeIdentifier();
647                     omode = nextModeIdentifier();
648                     checkOptionsAndRemainingInput();
649                     commandOption = at.hasOption("-command");
650                     quietOption = at.hasOption("-quiet");
651                     deleteOption = at.hasOption("-delete");
652                     retainOption = at.hasOption("-retain");
653                 }
654 
655                 void delete() {
656                     // Note: delete, for safety reasons, does NOT do name matching
657                     if (commandOption || quietOption) {
658                         errorat("jshell.err.conflicting.options");
659                     } else if (retainOption
660                             ? !retainedMap.containsKey(umode) && !modeMap.containsKey(umode)
661                             : !modeMap.containsKey(umode)) {
662                         // Cannot delete a mode that does not exist
663                         errorat("jshell.err.mode.unknown", umode);
664                     } else if (omode != null) {
665                         // old mode is for creation
666                         errorat("jshell.err.unexpected.at.end", omode);
667                     } else if (mode.name.equals(umode)) {
668                         // Cannot delete the current mode out from under us
669                         errorat("jshell.err.cannot.delete.current.mode", umode);
670                     } else if (retainOption && retainedCurrentMode != null &&
671                              retainedCurrentMode.name.equals(umode)) {
672                         // Cannot delete the retained mode or re-start will have an error
673                         errorat("jshell.err.cannot.delete.retained.mode", umode);
674                     } else {
675                         Mode m = modeMap.get(umode);
676                         if (m != null && m.readOnly) {
677                             errorat("jshell.err.not.valid.with.predefined.mode", umode);
678                         } else {
679                             // Remove the mode
680                             modeMap.remove(umode);
681                             if (retainOption) {
682                                 // Remove the retained mode
683                                 retainedMap.remove(umode);
684                                 updateRetainedModes();
685                             }
686                         }
687                     }
688                 }
689 
690                 void retain() {
691                     if (commandOption || quietOption) {
692                         errorat("jshell.err.conflicting.options");
693                     } else if (omode != null) {
694                         // old mode is for creation
695                         errorat("jshell.err.unexpected.at.end", omode);
696                     } else {
697                         Mode m = modeMap.get(umode);
698                         if (m == null) {
699                             // can only retain existing modes
700                             errorat("jshell.err.mode.unknown", umode);
701                         } else if (m.readOnly) {
702                             errorat("jshell.err.not.valid.with.predefined.mode", umode);
703                         } else {
704                             // Add to local cache of retained current encodings
705                             retainedMap.put(m.name, m.encode());
706                             updateRetainedModes();
707                         }
708                     }
709                 }
710 
711                 void updateRetainedModes() {
712                     // Join all the retained encodings
713                     String encoded = String.join(RECORD_SEPARATOR, retainedMap.values());
714                     // Retain it
715                     retainer.accept(encoded);
716                 }
717 
718                 void create() {
719                     if (commandOption && quietOption) {
720                         errorat("jshell.err.conflicting.options");
721                     } else if (!commandOption && !quietOption) {
722                         errorat("jshell.err.mode.creation");
723                     } else if (modeMap.containsKey(umode)) {
724                         // Mode already exists
725                         errorat("jshell.err.mode.exists", umode);
726                     } else {
727                         Mode om = searchForMode(omode);
728                         if (valid) {
729                             // We are copying an existing mode and/or creating a
730                             // brand-new mode -- in either case create from scratch
731                             Mode m = (om != null)
732                                     ? new Mode(umode, om)
733                                     : new Mode(umode);
734                             modeMap.put(umode, m);
735                             fluffmsg("jshell.msg.feedback.new.mode", m.name);
736                             m.setCommandFluff(commandOption);
737                         }
738                     }
739                 }
740 
741                 boolean set() {
742                     if (valid && !commandOption && !quietOption && !deleteOption &&
743                             omode == null && !retainOption) {
744                         // Not a creation, deletion, or retain -- show mode(s)
745                         showModeSettings(umode, "jshell.err.mode.creation");
746                     } else if (valid && umode == null) {
747                         errorat("jshell.err.missing.mode");
748                     } else if (valid && deleteOption) {
749                         delete();
750                     } else if (valid && retainOption) {
751                         retain();
752                     } else if (valid) {
753                         create();
754                     }
755                     if (!valid) {
756                         fluffmsg("jshell.msg.see", "/help /set mode");
757                     }
758                     return valid;
759                 }
760             }
761             return new SetMode().set();
762         }
763 
764         // For /set format <mode> <field> "<format>" <selector>...
setFormat()765         boolean setFormat() {
766             Mode m = nextMode();
767             String field = toIdentifier(next(), "jshell.err.field.name");
768             String format = nextFormat();
769             if (valid && format == null) {
770                 if (field != null && m != null && !m.byField.containsKey(field)) {
771                     errorat("jshell.err.field.name", field);
772                 } else {
773                     showFormatSettings(m, field);
774                 }
775             } else {
776                 installFormat(m, field, format, "/help /set format");
777             }
778             return valid;
779         }
780 
781         // For /set truncation <mode> <length> <selector>...
setTruncation()782         boolean setTruncation() {
783             Mode m = nextMode();
784             String length = next();
785             if (length == null) {
786                 showTruncationSettings(m);
787             } else {
788                 try {
789                     // Assure that integer format is correct
790                     Integer.parseUnsignedInt(length);
791                 } catch (NumberFormatException ex) {
792                     errorat("jshell.err.truncation.length.not.integer", length);
793                 }
794                 // install length into an internal format field
795                 installFormat(m, TRUNCATION_FIELD, length, "/help /set truncation");
796             }
797             return valid;
798         }
799 
800         // For /set feedback <mode>
setFeedback(Consumer<String> retainer)801         boolean setFeedback(Consumer<String> retainer) {
802             String umode = next();
803             checkOptionsAndRemainingInput();
804             boolean retainOption = at.hasOption("-retain");
805             if (valid && umode == null && !retainOption) {
806                 showFeedbackSetting();
807                 hard("");
808                 showFeedbackModes();
809                 return true;
810             }
811             if (valid) {
812                 Mode m = umode == null
813                         ? mode
814                         : searchForMode(toModeIdentifier(umode));
815                 if (valid && retainOption && !m.readOnly && !retainedMap.containsKey(m.name)) {
816                     errorat("jshell.err.retained.feedback.mode.must.be.retained.or.predefined");
817                 }
818                 if (valid) {
819                     if (umode != null) {
820                         mode = m;
821                         fluffmsg("jshell.msg.feedback.mode", mode.name);
822                     }
823                     if (retainOption) {
824                         retainedCurrentMode = m;
825                         retainer.accept(m.name);
826                     }
827                 }
828             }
829             if (!valid) {
830                 fluffmsg("jshell.msg.see", "/help /set feedback");
831                 return false;
832             }
833             return true;
834         }
835 
restoreEncodedModes(String allEncoded)836         boolean restoreEncodedModes(String allEncoded) {
837             try {
838                 // Iterate over each record in each encoded mode
839                 Iterator<String> itr = encodedModeIterator(allEncoded);
840                 while (itr.hasNext()) {
841                     // Reconstruct the encoded mode
842                     Mode m = buildMode(itr);
843                     modeMap.put(m.name, m);
844                     // Continue to retain if a new retains occur
845                     retainedMap.put(m.name, m.encode());
846                 }
847                 return true;
848             } catch (Throwable exc) {
849                 // Catastrophic corruption -- clear map
850                 errorat("jshell.err.retained.mode.failure", exc);
851                 retainedMap.clear();
852                 return false;
853             }
854         }
855 
856 
857         /**
858          * Set up a mode reconstituted from a preferences string.
859          *
860          * @param it the encoded Mode broken into String chunks, may contain
861          *           subsequent encoded modes
862          */
buildMode(Iterator<String> it)863         private Mode buildMode(Iterator<String> it) {
864             Mode newMode = new Mode(it.next(), Boolean.parseBoolean(it.next()),  it.next(), it.next());
865             Map<String, List<Mode.Setting>> fields = new HashMap<>();
866             long suspiciousBits = Selector.OLD_ALWAYS.asBits();
867             boolean suspicious = false;
868             String field;
869             while (!(field = it.next()).equals("***")) {
870                 String open = it.next();
871                 assert open.equals("(");
872                 List<Mode.Setting> settings = new ArrayList<>();
873                 String selectorText;
874                 while (!(selectorText = it.next()).equals(")")) {
875                     String format = it.next();
876                     Selector selector;
877                     if (selectorText.isEmpty()) {
878                         selector = Selector.ALWAYS;
879                     } else if (Character.isDigit(selectorText.charAt(0))) {
880                         // legacy format, bits
881                         long bits = Long.parseLong(selectorText);
882                         suspicious |= bits == suspiciousBits;
883                         selector = new Selector(bits);
884                     } else {
885                         selector = parseSelector(selectorText);
886                     }
887                     Mode.Setting ing = new Mode.Setting(format, selector);
888                     settings.add(ing);
889                 }
890                 fields.put(field, settings);
891             }
892             List<Mode.Setting> tk;
893             List<Mode.Setting> errf;
894             // If suspicious that this is a pre-JDK-14 settings, check deeper...
895             if (suspicious
896                     // Super simple might not define typeKind, otherwise check for JDK-14 presence of record
897                     && ((tk = fields.get("typeKind")) == null
898                     || !tk.stream().anyMatch(tkc -> tkc.selector.equals(RECORD_TYPEKIND_SELECTOR)))
899                     // no record typeKind, now check for corruption
900                     && ((errf = fields.get("err")) == null
901                     || errf.stream().anyMatch(tkc -> tkc.selector.equals(Selector.OLD_ALWAYS)))) {
902                 // Pre-JDK-14 setting found, convert them
903 
904                 // start with solid base, ideally normal
905                 Mode base = modeMap.get("normal");
906                 if (base == null) {
907                     base = mode;
908                 }
909 
910                 // Make sure any current fields/selectors are covered: filling in with the base (normal)
911                 base.byField.forEach((fieldName, settingList) ->
912                         settingList.forEach(setting -> newMode.set(fieldName, setting.format, setting.selector)));
913 
914                 // Now, overlay with user's settings (position adjusted).
915                 // Assume any setting for class applies to record, except for typeKind definition where base definition
916                 // should fall through.
917                 fields.forEach((fieldName, settingList) -> {
918                         settingList.forEach(setting -> newMode.set(fieldName, setting.format,
919                                 Selector.fromPreJDK14(setting.selector, !fieldName.equals("typeKind"))));
920                         });
921             } else {
922                 fields.forEach((fieldName, settingList) ->
923                         settingList.forEach(setting -> newMode.set(fieldName, setting.format, setting.selector)));
924             }
925             return newMode;
926         }
927 
encodedModeIterator(String encoded)928         Iterator<String> encodedModeIterator(String encoded) {
929             String[] ms = encoded.split(RECORD_SEPARATOR);
930             return Arrays.asList(ms).iterator();
931         }
932 
933         // install the format of a field under parsed selectors
installFormat(Mode m, String field, String format, String help)934         void installFormat(Mode m, String field, String format, String help) {
935             String slRaw;
936             List<Selector> selectorList = new ArrayList<>();
937             while (valid && (slRaw = next()) != null) {
938                 selectorList.add(parseSelector(slRaw));
939             }
940             checkOptionsAndRemainingInput();
941             if (valid) {
942                 if (m.readOnly) {
943                     errorat("jshell.err.not.valid.with.predefined.mode", m.name);
944                 } else if (selectorList.isEmpty()) {
945                     // No selectors specified, then always use the format
946                     m.set(field, format, Selector.ALWAYS);
947                 } else {
948                     // Set the format of the field for specified selector
949                     selectorList.forEach(sel -> m.set(field, format, sel));
950                 }
951             } else {
952                 fluffmsg("jshell.msg.see", help);
953             }
954         }
955 
checkOptionsAndRemainingInput()956         void checkOptionsAndRemainingInput() {
957             String junk = at.remainder();
958             if (!junk.isEmpty()) {
959                 errorat("jshell.err.unexpected.at.end", junk);
960             } else {
961                 String bad = at.badOptions();
962                 if (!bad.isEmpty()) {
963                     errorat("jshell.err.unknown.option", bad);
964                 }
965             }
966         }
967 
next()968         String next() {
969             String s = at.next();
970             if (s == null) {
971                 checkOptionsAndRemainingInput();
972             }
973             return s;
974         }
975 
976         /**
977          * Check that the specified string is an identifier (Java identifier).
978          * If null display the missing error. If it is not an identifier,
979          * display the error.
980          *
981          * @param id the string to check, MUST be the most recently retrieved
982          * token from 'at'.
983          * @param err the resource error to display if not an identifier
984          * @return the identifier string, or null if null or not an identifier
985          */
toIdentifier(String id, String err)986         private String toIdentifier(String id, String err) {
987             if (!valid || id == null) {
988                 return null;
989             }
990             if (at.isQuoted() ||
991                     !id.codePoints().allMatch(Character::isJavaIdentifierPart)) {
992                 errorat(err, id);
993                 return null;
994             }
995             return id;
996         }
997 
toModeIdentifier(String id)998         private String toModeIdentifier(String id) {
999             return toIdentifier(id, "jshell.err.mode.name");
1000         }
1001 
nextModeIdentifier()1002         private String nextModeIdentifier() {
1003             return toModeIdentifier(next());
1004         }
1005 
nextMode()1006         private Mode nextMode() {
1007             String umode = nextModeIdentifier();
1008             return searchForMode(umode);
1009         }
1010 
searchForMode(String umode)1011         private Mode searchForMode(String umode) {
1012             return searchForMode(umode, null);
1013         }
1014 
searchForMode(String umode, String msg)1015         private Mode searchForMode(String umode, String msg) {
1016             if (!valid || umode == null) {
1017                 return null;
1018             }
1019             Mode m = modeMap.get(umode);
1020             if (m != null) {
1021                 return m;
1022             }
1023             // Failing an exact match, go searching
1024             Mode[] matches = modeMap.entrySet().stream()
1025                     .filter(e -> e.getKey().startsWith(umode))
1026                     .map(Entry::getValue)
1027                     .toArray(Mode[]::new);
1028             if (matches.length == 1) {
1029                 return matches[0];
1030             } else {
1031                 if (msg != null) {
1032                     hardmsg(msg, "");
1033                 }
1034                 if (matches.length == 0) {
1035                     errorat("jshell.err.feedback.does.not.match.mode", umode);
1036                 } else {
1037                     errorat("jshell.err.feedback.ambiguous.mode", umode);
1038                 }
1039                 if (showFluff()) {
1040                     showFeedbackModes();
1041                 }
1042                 return null;
1043             }
1044         }
1045 
showFeedbackModes()1046         void showFeedbackModes() {
1047             if (!retainedMap.isEmpty()) {
1048                 hardmsg("jshell.msg.feedback.retained.mode.following");
1049                 retainedMap.keySet().stream()
1050                         .sorted()
1051                         .forEach(mk -> hard("   %s", mk));
1052             }
1053             hardmsg("jshell.msg.feedback.mode.following");
1054             modeMap.keySet().stream()
1055                     .sorted()
1056                     .forEach(mk -> hard("   %s", mk));
1057         }
1058 
1059         // Read and test if the format string is correctly
nextFormat()1060         private String nextFormat() {
1061             return toFormat(next());
1062         }
1063 
1064         // Test if the format string is correctly
toFormat(String format)1065         private String toFormat(String format) {
1066             if (!valid || format == null) {
1067                 return null;
1068             }
1069             if (!at.isQuoted()) {
1070                 errorat("jshell.err.feedback.must.be.quoted", format);
1071                return null;
1072             }
1073             return format;
1074         }
1075 
1076         // Convert to a quoted string
toStringLiteral(String s)1077         private String toStringLiteral(String s) {
1078             StringBuilder sb = new StringBuilder();
1079             sb.append('"');
1080             final int length = s.length();
1081             for (int offset = 0; offset < length;) {
1082                 final int codepoint = s.codePointAt(offset);
1083 
1084                 switch (codepoint) {
1085                     case '\b':
1086                         sb.append("\\b");
1087                         break;
1088                     case '\t':
1089                         sb.append("\\t");
1090                         break;
1091                     case '\n':
1092                         sb.append("\\n");
1093                         break;
1094                     case '\f':
1095                         sb.append("\\f");
1096                         break;
1097                     case '\r':
1098                         sb.append("\\r");
1099                         break;
1100                     case '\"':
1101                         sb.append("\\\"");
1102                         break;
1103                     case '\'':
1104                         sb.append("\\'");
1105                         break;
1106                     case '\\':
1107                         sb.append("\\\\");
1108                         break;
1109                     default:
1110                         if (codepoint < 040) {
1111                             sb.append(String.format("\\%o", codepoint));
1112                         } else {
1113                             sb.appendCodePoint(codepoint);
1114                         }
1115                         break;
1116                 }
1117 
1118                 // do something with the codepoint
1119                 offset += Character.charCount(codepoint);
1120 
1121             }
1122             sb.append('"');
1123             return sb.toString();
1124         }
1125 
parseSelector(String selectorText)1126         private Selector parseSelector(String selectorText) {
1127             SelectorBuilder seb = new SelectorBuilder(selectorText);
1128             EnumSet<SelectorKind> seen = EnumSet.noneOf(SelectorKind.class);
1129             for (String s : selectorText.split("-")) {
1130                 SelectorKind lastKind = null;
1131                 for (String as : s.split(",")) {
1132                     if (!as.isEmpty()) {
1133                         SelectorInstanceWithDoc<?> sel = Selector.selectorMap.get(as);
1134                         if (sel == null) {
1135                             errorat("jshell.err.feedback.not.a.valid.selector", as, s);
1136                             return Selector.ALWAYS;
1137                         }
1138                         SelectorKind kind = sel.kind();
1139                         if (lastKind == null) {
1140                             if (seen.contains(kind)) {
1141                                 errorat("jshell.err.feedback.multiple.sections", as, s);
1142                                 return Selector.ALWAYS;
1143                             }
1144                         } else if (kind != lastKind) {
1145                             errorat("jshell.err.feedback.different.selector.kinds", as, s);
1146                             return Selector.ALWAYS;
1147                         }
1148                         seb.add(sel);
1149                         seen.add(kind);
1150                         lastKind = kind;
1151                     }
1152                 }
1153             }
1154             return seb.toSelector();
1155          }
1156     }
1157 }
1158