Feedback.java revision 3827:44bdefe64114
1/*
2 * Copyright (c) 2016, 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
26package jdk.internal.jshell.tool;
27
28import java.util.ArrayList;
29import java.util.Arrays;
30import java.util.Collection;
31import java.util.Collections;
32import java.util.EnumSet;
33import java.util.HashMap;
34import java.util.HashSet;
35import java.util.Iterator;
36import java.util.List;
37import java.util.Locale;
38import java.util.Map;
39import java.util.Map.Entry;
40import java.util.Objects;
41import java.util.Set;
42import java.util.StringJoiner;
43import java.util.function.BiConsumer;
44import java.util.function.BinaryOperator;
45import java.util.function.Consumer;
46import java.util.function.Function;
47import java.util.function.Supplier;
48import java.util.regex.Matcher;
49import java.util.regex.Pattern;
50import java.util.stream.Collector;
51import static java.util.stream.Collectors.joining;
52import static java.util.stream.Collectors.toMap;
53import static jdk.internal.jshell.tool.ContinuousCompletionProvider.PERFECT_MATCHER;
54import jdk.internal.jshell.tool.JShellTool.CompletionProvider;
55import static jdk.internal.jshell.tool.JShellTool.EMPTY_COMPLETION_PROVIDER;
56
57/**
58 * Feedback customization support
59 *
60 * @author Robert Field
61 */
62class Feedback {
63
64    // Patern for substituted fields within a customized format string
65    private static final Pattern FIELD_PATTERN = Pattern.compile("\\{(.*?)\\}");
66
67    // Internal field name for truncation length
68    private static final String TRUNCATION_FIELD = "<truncation>";
69
70    // For encoding to Properties String
71    private static final String RECORD_SEPARATOR = "\u241E";
72
73    // Current mode -- initial value is placeholder during start-up
74    private Mode mode = new Mode("");
75
76    // Retained current mode -- for checks
77    private Mode retainedCurrentMode = null;
78
79    // Mapping of mode name to mode
80    private final Map<String, Mode> modeMap = new HashMap<>();
81
82    // Mapping of mode names to encoded retained mode
83    private final Map<String, String> retainedMap = new HashMap<>();
84
85    // Mapping selector enum names to enums
86    private final Map<String, Selector<?>> selectorMap = new HashMap<>();
87
88    private static final long ALWAYS = bits(FormatCase.all, FormatAction.all, FormatWhen.all,
89            FormatResolve.all, FormatUnresolved.all, FormatErrors.all);
90    private static final long ANY = 0L;
91
92    public boolean shouldDisplayCommandFluff() {
93        return mode.commandFluff;
94    }
95
96    public String getPre() {
97        return mode.format("pre", ANY);
98    }
99
100    public String getPost() {
101        return mode.format("post", ANY);
102    }
103
104    public String getErrorPre() {
105        return mode.format("errorpre", ANY);
106    }
107
108    public String getErrorPost() {
109        return mode.format("errorpost", ANY);
110    }
111
112    public String format(FormatCase fc, FormatAction fa, FormatWhen fw,
113                    FormatResolve fr, FormatUnresolved fu, FormatErrors fe,
114                    String name, String type, String value, String unresolved, List<String> errorLines) {
115        return mode.format(fc, fa, fw, fr, fu, fe,
116                name, type, value, unresolved, errorLines);
117    }
118
119    public String truncateVarValue(String value) {
120        return mode.truncateVarValue(value);
121    }
122
123    public String getPrompt(String nextId) {
124        return mode.getPrompt(nextId);
125    }
126
127    public String getContinuationPrompt(String nextId) {
128        return mode.getContinuationPrompt(nextId);
129    }
130
131    public boolean setFeedback(MessageHandler messageHandler, ArgTokenizer at, Consumer<String> retainer) {
132        return new Setter(messageHandler, at).setFeedback(retainer);
133    }
134
135    public boolean setFormat(MessageHandler messageHandler, ArgTokenizer at) {
136        return new Setter(messageHandler, at).setFormat();
137    }
138
139    public boolean setTruncation(MessageHandler messageHandler, ArgTokenizer at) {
140        return new Setter(messageHandler, at).setTruncation();
141    }
142
143    public boolean setMode(MessageHandler messageHandler, ArgTokenizer at, Consumer<String> retainer) {
144        return new Setter(messageHandler, at).setMode(retainer);
145    }
146
147    public boolean setPrompt(MessageHandler messageHandler, ArgTokenizer at) {
148        return new Setter(messageHandler, at).setPrompt();
149    }
150
151    public boolean restoreEncodedModes(MessageHandler messageHandler, String encoded) {
152        return new Setter(messageHandler, new ArgTokenizer("<init>", "")).restoreEncodedModes(encoded);
153    }
154
155    public void markModesReadOnly() {
156        modeMap.values().stream()
157                .forEach(m -> m.readOnly = true);
158    }
159
160    JShellTool.CompletionProvider modeCompletions() {
161        return modeCompletions(EMPTY_COMPLETION_PROVIDER);
162    }
163
164    JShellTool.CompletionProvider modeCompletions(CompletionProvider successor) {
165        return new ContinuousCompletionProvider(
166                () -> modeMap.keySet().stream()
167                        .collect(toMap(Function.identity(), m -> successor)),
168                PERFECT_MATCHER);
169    }
170
171    {
172        for (FormatCase e : FormatCase.all)
173            selectorMap.put(e.name().toLowerCase(Locale.US), e);
174        for (FormatAction e : FormatAction.all)
175            selectorMap.put(e.name().toLowerCase(Locale.US), e);
176        for (FormatResolve e : FormatResolve.all)
177            selectorMap.put(e.name().toLowerCase(Locale.US), e);
178        for (FormatUnresolved e : FormatUnresolved.all)
179            selectorMap.put(e.name().toLowerCase(Locale.US), e);
180        for (FormatErrors e : FormatErrors.all)
181            selectorMap.put(e.name().toLowerCase(Locale.US), e);
182        for (FormatWhen e : FormatWhen.all)
183            selectorMap.put(e.name().toLowerCase(Locale.US), e);
184    }
185
186    private static class SelectorSets {
187        Set<FormatCase> cc;
188        Set<FormatAction> ca;
189        Set<FormatWhen> cw;
190        Set<FormatResolve> cr;
191        Set<FormatUnresolved> cu;
192        Set<FormatErrors> ce;
193    }
194
195    /**
196     * Holds all the context of a mode mode
197     */
198    private static class Mode {
199
200        // Name of mode
201        final String name;
202
203        // Display command verification/information
204        boolean commandFluff;
205
206        // Event cases: class, method, expression, ...
207        final Map<String, List<Setting>> cases;
208
209        boolean readOnly = false;
210
211        String prompt = "\n-> ";
212        String continuationPrompt = ">> ";
213
214        static class Setting {
215
216            final long enumBits;
217            final String format;
218
219            Setting(long enumBits, String format) {
220                this.enumBits = enumBits;
221                this.format = format;
222            }
223
224            @Override
225            public boolean equals(Object o) {
226                if (o instanceof Setting) {
227                    Setting ing = (Setting) o;
228                    return enumBits == ing.enumBits && format.equals(ing.format);
229                } else {
230                    return false;
231                }
232            }
233
234            @Override
235            public int hashCode() {
236                int hash = 7;
237                hash = 67 * hash + (int) (this.enumBits ^ (this.enumBits >>> 32));
238                hash = 67 * hash + Objects.hashCode(this.format);
239                return hash;
240            }
241        }
242
243        /**
244         * Set up an empty mode.
245         *
246         * @param name
247         * @param commandFluff True if should display command fluff messages
248         */
249        Mode(String name) {
250            this.name = name;
251            this.cases = new HashMap<>();
252            add("name",       new Setting(ALWAYS, "%1$s"));
253            add("type",       new Setting(ALWAYS, "%2$s"));
254            add("value",      new Setting(ALWAYS, "%3$s"));
255            add("unresolved", new Setting(ALWAYS, "%4$s"));
256            add("errors",     new Setting(ALWAYS, "%5$s"));
257            add("err",        new Setting(ALWAYS, "%6$s"));
258
259            add("errorline",  new Setting(ALWAYS, "    {err}%n"));
260
261            add("pre",        new Setting(ALWAYS, "|  "));
262            add("post",       new Setting(ALWAYS, "%n"));
263            add("errorpre",   new Setting(ALWAYS, "|  "));
264            add("errorpost",  new Setting(ALWAYS, "%n"));
265        }
266
267        /**
268         * Set up a copied mode.
269         *
270         * @param name
271         * @param m Mode to copy, or null for no fresh
272         */
273        Mode(String name, Mode m) {
274            this.name = name;
275            this.commandFluff = m.commandFluff;
276            this.prompt = m.prompt;
277            this.continuationPrompt = m.continuationPrompt;
278            this.cases = new HashMap<>();
279            m.cases.entrySet().stream()
280                    .forEach(fes -> fes.getValue()
281                    .forEach(ing -> add(fes.getKey(), ing)));
282
283        }
284
285        /**
286         * Set up a mode reconstituted from a preferences string.
287         *
288         * @param it the encoded Mode broken into String chunks, may contain
289         * subsequent encoded modes
290         */
291        Mode(Iterator<String> it) {
292            this.name = it.next();
293            this.commandFluff = Boolean.parseBoolean(it.next());
294            this.prompt = it.next();
295            this.continuationPrompt = it.next();
296            cases = new HashMap<>();
297            String field;
298            while (!(field = it.next()).equals("***")) {
299                String open = it.next();
300                assert open.equals("(");
301                List<Setting> settings = new ArrayList<>();
302                String bits;
303                while (!(bits = it.next()).equals(")")) {
304                    String format = it.next();
305                    Setting ing = new Setting(Long.parseLong(bits), format);
306                    settings.add(ing);
307                }
308                cases.put(field, settings);
309            }
310        }
311
312        @Override
313        public boolean equals(Object o) {
314            if (o instanceof Mode) {
315                Mode m = (Mode) o;
316                return name.equals((m.name))
317                        && commandFluff == m.commandFluff
318                        && prompt.equals((m.prompt))
319                        && continuationPrompt.equals((m.continuationPrompt))
320                        && cases.equals((m.cases));
321            } else {
322                return false;
323            }
324        }
325
326        @Override
327        public int hashCode() {
328            return Objects.hashCode(name);
329        }
330
331        /**
332         * Set if this mode displays informative/confirmational messages on
333         * commands.
334         *
335         * @param fluff the value to set
336         */
337        void setCommandFluff(boolean fluff) {
338            commandFluff = fluff;
339        }
340
341        /**
342         * Encodes the mode into a String so it can be saved in Preferences.
343         *
344         * @return the string representation
345         */
346        String encode() {
347            List<String> el = new ArrayList<>();
348            el.add(name);
349            el.add(String.valueOf(commandFluff));
350            el.add(prompt);
351            el.add(continuationPrompt);
352            for (Entry<String, List<Setting>> es : cases.entrySet()) {
353                el.add(es.getKey());
354                el.add("(");
355                for (Setting ing : es.getValue()) {
356                    el.add(String.valueOf(ing.enumBits));
357                    el.add(ing.format);
358                }
359                el.add(")");
360            }
361            el.add("***");
362            return String.join(RECORD_SEPARATOR, el);
363        }
364
365        private void add(String field, Setting ing) {
366            List<Setting> settings = cases.get(field);
367            if (settings == null) {
368                settings = new ArrayList<>();
369                cases.put(field, settings);
370            } else {
371                // remove obscured settings
372                long mask = ~ing.enumBits;
373                settings.removeIf(t -> (t.enumBits & mask) == 0);
374            }
375            settings.add(ing);
376        }
377
378        void set(String field,
379                Collection<FormatCase> cc, Collection<FormatAction> ca, Collection<FormatWhen> cw,
380                Collection<FormatResolve> cr, Collection<FormatUnresolved> cu, Collection<FormatErrors> ce,
381                String format) {
382            long bits = bits(cc, ca, cw, cr, cu, ce);
383            set(field, bits, format);
384        }
385
386        void set(String field, long bits, String format) {
387            add(field, new Setting(bits, format));
388        }
389
390        /**
391         * Lookup format Replace fields with context specific formats.
392         *
393         * @return format string
394         */
395        String format(String field, long bits) {
396            List<Setting> settings = cases.get(field);
397            if (settings == null) {
398                return ""; //TODO error?
399            }
400            String format = null;
401            for (int i = settings.size() - 1; i >= 0; --i) {
402                Setting ing = settings.get(i);
403                long mask = ing.enumBits;
404                if ((bits & mask) == bits) {
405                    format = ing.format;
406                    break;
407                }
408            }
409            if (format == null || format.isEmpty()) {
410                return "";
411            }
412            Matcher m = FIELD_PATTERN.matcher(format);
413            StringBuffer sb = new StringBuffer(format.length());
414            while (m.find()) {
415                String fieldName = m.group(1);
416                String sub = format(fieldName, bits);
417                m.appendReplacement(sb, Matcher.quoteReplacement(sub));
418            }
419            m.appendTail(sb);
420            return sb.toString();
421        }
422
423        String truncateVarValue(String value) {
424            return truncateValue(value,
425                    bits(FormatCase.VARVALUE, FormatAction.ADDED,
426                            FormatWhen.PRIMARY, FormatResolve.OK,
427                            FormatUnresolved.UNRESOLVED0, FormatErrors.ERROR0));
428        }
429
430        String truncateValue(String value, long bits) {
431            if (value==null) {
432                return "";
433            } else {
434                // Retrieve the truncation length
435                String truncField = format(TRUNCATION_FIELD, bits);
436                if (truncField.isEmpty()) {
437                    // No truncation set, use whole value
438                    return value;
439                } else {
440                    // Convert truncation length to int
441                    // this is safe since it has been tested before it is set
442                    int trunc = Integer.parseUnsignedInt(truncField);
443                    int len = value.length();
444                    if (len > trunc) {
445                        if (trunc <= 13) {
446                            // Very short truncations have no room for "..."
447                            return value.substring(0, trunc);
448                        } else {
449                            // Normal truncation, make total length equal truncation length
450                            int endLen = trunc / 3;
451                            int startLen = trunc - 5 - endLen;
452                            return value.substring(0, startLen) + " ... " + value.substring(len -endLen);
453                        }
454                    } else {
455                        // Within truncation length, use whole value
456                        return value;
457                    }
458                }
459            }
460        }
461
462        // Compute the display output given full context and values
463        String format(FormatCase fc, FormatAction fa, FormatWhen fw,
464                    FormatResolve fr, FormatUnresolved fu, FormatErrors fe,
465                    String name, String type, String value, String unresolved, List<String> errorLines) {
466            // Convert the context into a bit representation used as selectors for store field formats
467            long bits = bits(fc, fa, fw, fr, fu, fe);
468            String fname = name==null? "" : name;
469            String ftype = type==null? "" : type;
470            // Compute the representation of value
471            String fvalue = truncateValue(value, bits);
472            String funresolved = unresolved==null? "" : unresolved;
473            String errors = errorLines.stream()
474                    .map(el -> String.format(
475                            format("errorline", bits),
476                            fname, ftype, fvalue, funresolved, "*cannot-use-errors-here*", el))
477                    .collect(joining());
478            return String.format(
479                    format("display", bits),
480                    fname, ftype, fvalue, funresolved, errors, "*cannot-use-err-here*");
481        }
482
483        void setPrompts(String prompt, String continuationPrompt) {
484            this.prompt = prompt;
485            this.continuationPrompt = continuationPrompt;
486        }
487
488        String getPrompt(String nextId) {
489            return String.format(prompt, nextId);
490        }
491
492        String getContinuationPrompt(String nextId) {
493            return String.format(continuationPrompt, nextId);
494        }
495    }
496
497    // Representation of one instance of all the enum values as bits in a long
498    private static long bits(FormatCase fc, FormatAction fa, FormatWhen fw,
499            FormatResolve fr, FormatUnresolved fu, FormatErrors fe) {
500        long res = 0L;
501        res |= 1 << fc.ordinal();
502        res <<= FormatAction.count;
503        res |= 1 << fa.ordinal();
504        res <<= FormatWhen.count;
505        res |= 1 << fw.ordinal();
506        res <<= FormatResolve.count;
507        res |= 1 << fr.ordinal();
508        res <<= FormatUnresolved.count;
509        res |= 1 << fu.ordinal();
510        res <<= FormatErrors.count;
511        res |= 1 << fe.ordinal();
512        return res;
513    }
514
515    // Representation of a space of enum values as or'edbits in a long
516    private static long bits(Collection<FormatCase> cc, Collection<FormatAction> ca, Collection<FormatWhen> cw,
517                Collection<FormatResolve> cr, Collection<FormatUnresolved> cu, Collection<FormatErrors> ce) {
518        long res = 0L;
519        for (FormatCase fc : cc)
520            res |= 1 << fc.ordinal();
521        res <<= FormatAction.count;
522        for (FormatAction fa : ca)
523            res |= 1 << fa.ordinal();
524        res <<= FormatWhen.count;
525        for (FormatWhen fw : cw)
526            res |= 1 << fw.ordinal();
527        res <<= FormatResolve.count;
528        for (FormatResolve fr : cr)
529            res |= 1 << fr.ordinal();
530        res <<= FormatUnresolved.count;
531        for (FormatUnresolved fu : cu)
532            res |= 1 << fu.ordinal();
533        res <<= FormatErrors.count;
534        for (FormatErrors fe : ce)
535            res |= 1 << fe.ordinal();
536        return res;
537    }
538
539    private static SelectorSets unpackEnumbits(long enumBits) {
540        class Unpacker {
541
542            SelectorSets u = new SelectorSets();
543            long b = enumBits;
544
545            <E extends Enum<E>> Set<E> unpackEnumbits(E[] values) {
546                Set<E> c = new HashSet<>();
547                for (int i = 0; i < values.length; ++i) {
548                    if ((b & (1 << i)) != 0) {
549                        c.add(values[i]);
550                    }
551                }
552                b >>>= values.length;
553                return c;
554            }
555
556            SelectorSets unpack() {
557                // inverseof the order they were packed
558                u.ce = unpackEnumbits(FormatErrors.values());
559                u.cu = unpackEnumbits(FormatUnresolved.values());
560                u.cr = unpackEnumbits(FormatResolve.values());
561                u.cw = unpackEnumbits(FormatWhen.values());
562                u.ca = unpackEnumbits(FormatAction.values());
563                u.cc = unpackEnumbits(FormatCase.values());
564                return u;
565            }
566        }
567        return new Unpacker().unpack();
568    }
569
570    interface Selector<E extends Enum<E> & Selector<E>> {
571        SelectorCollector<E> collector(Setter.SelectorList sl);
572        String doc();
573    }
574
575    /**
576     * The event cases
577     */
578    public enum FormatCase implements Selector<FormatCase> {
579        IMPORT("import declaration"),
580        CLASS("class declaration"),
581        INTERFACE("interface declaration"),
582        ENUM("enum declaration"),
583        ANNOTATION("annotation interface declaration"),
584        METHOD("method declaration -- note: {type}==parameter-types"),
585        VARDECL("variable declaration without init"),
586        VARINIT("variable declaration with init"),
587        EXPRESSION("expression -- note: {name}==scratch-variable-name"),
588        VARVALUE("variable value expression"),
589        ASSIGNMENT("assign variable"),
590        STATEMENT("statement");
591        String doc;
592        static final EnumSet<FormatCase> all = EnumSet.allOf(FormatCase.class);
593        static final int count = all.size();
594
595        @Override
596        public SelectorCollector<FormatCase> collector(Setter.SelectorList sl) {
597            return sl.cases;
598        }
599
600        @Override
601        public String doc() {
602            return doc;
603        }
604
605        private FormatCase(String doc) {
606            this.doc = doc;
607        }
608    }
609
610    /**
611     * The event actions
612     */
613    public enum FormatAction implements Selector<FormatAction> {
614        ADDED("snippet has been added"),
615        MODIFIED("an existing snippet has been modified"),
616        REPLACED("an existing snippet has been replaced with a new snippet"),
617        OVERWROTE("an existing snippet has been overwritten"),
618        DROPPED("snippet has been dropped"),
619        USED("snippet was used when it cannot be");
620        String doc;
621        static final EnumSet<FormatAction> all = EnumSet.allOf(FormatAction.class);
622        static final int count = all.size();
623
624        @Override
625        public SelectorCollector<FormatAction> collector(Setter.SelectorList sl) {
626            return sl.actions;
627        }
628
629        @Override
630        public String doc() {
631            return doc;
632        }
633
634        private FormatAction(String doc) {
635            this.doc = doc;
636        }
637    }
638
639    /**
640     * When the event occurs: primary or update
641     */
642    public enum FormatWhen implements Selector<FormatWhen> {
643        PRIMARY("the entered snippet"),
644        UPDATE("an update to a dependent snippet");
645        String doc;
646        static final EnumSet<FormatWhen> all = EnumSet.allOf(FormatWhen.class);
647        static final int count = all.size();
648
649        @Override
650        public SelectorCollector<FormatWhen> collector(Setter.SelectorList sl) {
651            return sl.whens;
652        }
653
654        @Override
655        public String doc() {
656            return doc;
657        }
658
659        private FormatWhen(String doc) {
660            this.doc = doc;
661        }
662    }
663
664    /**
665     * Resolution problems
666     */
667    public enum FormatResolve implements Selector<FormatResolve> {
668        OK("resolved correctly"),
669        DEFINED("defined despite recoverably unresolved references"),
670        NOTDEFINED("not defined because of recoverably unresolved references");
671        String doc;
672        static final EnumSet<FormatResolve> all = EnumSet.allOf(FormatResolve.class);
673        static final int count = all.size();
674
675        @Override
676        public SelectorCollector<FormatResolve> collector(Setter.SelectorList sl) {
677            return sl.resolves;
678        }
679
680        @Override
681        public String doc() {
682            return doc;
683        }
684
685        private FormatResolve(String doc) {
686            this.doc = doc;
687        }
688    }
689
690    /**
691     * Count of unresolved references
692     */
693    public enum FormatUnresolved implements Selector<FormatUnresolved> {
694        UNRESOLVED0("no names are unresolved"),
695        UNRESOLVED1("one name is unresolved"),
696        UNRESOLVED2("two or more names are unresolved");
697        String doc;
698        static final EnumSet<FormatUnresolved> all = EnumSet.allOf(FormatUnresolved.class);
699        static final int count = all.size();
700
701        @Override
702        public SelectorCollector<FormatUnresolved> collector(Setter.SelectorList sl) {
703            return sl.unresolvedCounts;
704        }
705
706        @Override
707        public String doc() {
708            return doc;
709        }
710
711        private FormatUnresolved(String doc) {
712            this.doc = doc;
713        }
714    }
715
716    /**
717     * Count of unresolved references
718     */
719    public enum FormatErrors implements Selector<FormatErrors> {
720        ERROR0("no errors"),
721        ERROR1("one error"),
722        ERROR2("two or more errors");
723        String doc;
724        static final EnumSet<FormatErrors> all = EnumSet.allOf(FormatErrors.class);
725        static final int count = all.size();
726
727        @Override
728        public SelectorCollector<FormatErrors> collector(Setter.SelectorList sl) {
729            return sl.errorCounts;
730        }
731
732        @Override
733        public String doc() {
734            return doc;
735        }
736
737        private FormatErrors(String doc) {
738            this.doc = doc;
739        }
740    }
741
742    class SelectorCollector<E extends Enum<E> & Selector<E>> {
743        final EnumSet<E> all;
744        EnumSet<E> set = null;
745        SelectorCollector(EnumSet<E> all) {
746            this.all = all;
747        }
748        void add(Object o) {
749            @SuppressWarnings("unchecked")
750            E e = (E) o;
751            if (set == null) {
752                set = EnumSet.of(e);
753            } else {
754                set.add(e);
755            }
756        }
757
758        boolean isEmpty() {
759            return set == null;
760        }
761
762        EnumSet<E> getSet() {
763            return set == null
764                    ? all
765                    : set;
766        }
767    }
768
769    // Class used to set custom eval output formats
770    // For both /set format  -- Parse arguments, setting custom format, or printing error
771    private class Setter {
772
773        private final ArgTokenizer at;
774        private final MessageHandler messageHandler;
775        boolean valid = true;
776
777        Setter(MessageHandler messageHandler, ArgTokenizer at) {
778            this.messageHandler = messageHandler;
779            this.at = at;
780            at.allowedOptions("-retain");
781        }
782
783        void fluff(String format, Object... args) {
784            messageHandler.fluff(format, args);
785        }
786
787        void hard(String format, Object... args) {
788            messageHandler.hard(format, args);
789        }
790
791        void fluffmsg(String messageKey, Object... args) {
792            messageHandler.fluffmsg(messageKey, args);
793        }
794
795        void hardmsg(String messageKey, Object... args) {
796            messageHandler.hardmsg(messageKey, args);
797        }
798
799        boolean showFluff() {
800            return messageHandler.showFluff();
801        }
802
803        void errorat(String messageKey, Object... args) {
804            if (!valid) {
805                // no spew of errors
806                return;
807            }
808            valid = false;
809            Object[] a2 = Arrays.copyOf(args, args.length + 2);
810            a2[args.length] = at.whole();
811            messageHandler.errormsg(messageKey, a2);
812        }
813
814        String selectorsToString(SelectorSets u) {
815            StringBuilder sb = new StringBuilder();
816            selectorToString(sb, u.cc, FormatCase.values());
817            selectorToString(sb, u.ca, FormatAction.values());
818            selectorToString(sb, u.cw, FormatWhen.values());
819            selectorToString(sb, u.cr, FormatResolve.values());
820            selectorToString(sb, u.cu, FormatUnresolved.values());
821            selectorToString(sb, u.ce, FormatErrors.values());
822            return sb.toString();
823        }
824
825        private <E extends Enum<E>> void selectorToString(StringBuilder sb, Set<E> c, E[] values) {
826            if (!c.containsAll(Arrays.asList(values))) {
827                sb.append(c.stream()
828                        .sorted((x, y) -> x.ordinal() - y.ordinal())
829                        .map(v -> v.name().toLowerCase(Locale.US))
830                        .collect(new Collector<CharSequence, StringJoiner, String>() {
831                            @Override
832                            public BiConsumer<StringJoiner, CharSequence> accumulator() {
833                                return StringJoiner::add;
834                            }
835
836                            @Override
837                            public Supplier<StringJoiner> supplier() {
838                                return () -> new StringJoiner(",", (sb.length() == 0)? "" : "-", "")
839                                        .setEmptyValue("");
840                            }
841
842                            @Override
843                            public BinaryOperator<StringJoiner> combiner() {
844                                return StringJoiner::merge;
845                            }
846
847                            @Override
848                            public Function<StringJoiner, String> finisher() {
849                                return StringJoiner::toString;
850                            }
851
852                            @Override
853                            public Set<Characteristics> characteristics() {
854                                return Collections.emptySet();
855                            }
856                        }));
857            }
858        }
859
860        // Show format settings -- in a predictable order, for testing...
861        void showFormatSettings(Mode sm, String f) {
862            if (sm == null) {
863                modeMap.entrySet().stream()
864                        .sorted((es1, es2) -> es1.getKey().compareTo(es2.getKey()))
865                        .forEach(m -> showFormatSettings(m.getValue(), f));
866            } else {
867                sm.cases.entrySet().stream()
868                        .filter(ec -> (f == null)
869                            ? !ec.getKey().equals(TRUNCATION_FIELD)
870                            : ec.getKey().equals(f))
871                        .sorted((ec1, ec2) -> ec1.getKey().compareTo(ec2.getKey()))
872                        .forEach(ec -> {
873                            ec.getValue().forEach(s -> {
874                                hard("/set format %s %s %s %s",
875                                        sm.name, ec.getKey(), toStringLiteral(s.format),
876                                        selectorsToString(unpackEnumbits(s.enumBits)));
877
878                            });
879                        });
880            }
881        }
882
883        void showTruncationSettings(Mode sm) {
884            if (sm == null) {
885                modeMap.values().forEach(this::showTruncationSettings);
886            } else {
887                List<Mode.Setting> trunc = sm.cases.get(TRUNCATION_FIELD);
888                if (trunc != null) {
889                    trunc.forEach(s -> {
890                        hard("/set truncation %s %s %s",
891                                sm.name, s.format,
892                                selectorsToString(unpackEnumbits(s.enumBits)));
893                    });
894                }
895            }
896        }
897
898        void showPromptSettings(Mode sm) {
899            if (sm == null) {
900                modeMap.values().forEach(this::showPromptSettings);
901            } else {
902                hard("/set prompt %s %s %s",
903                        sm.name,
904                        toStringLiteral(sm.prompt),
905                        toStringLiteral(sm.continuationPrompt));
906            }
907        }
908
909        void showModeSettings(String umode, String msg) {
910            if (umode == null) {
911                modeMap.values().forEach(this::showModeSettings);
912            } else {
913                Mode m;
914                String retained = retainedMap.get(umode);
915                if (retained == null) {
916                    m = searchForMode(umode, msg);
917                    if (m == null) {
918                        return;
919                    }
920                    umode = m.name;
921                    retained = retainedMap.get(umode);
922                } else {
923                    m = modeMap.get(umode);
924                }
925                if (retained != null) {
926                    Mode rm = new Mode(encodedModeIterator(retained));
927                    showModeSettings(rm);
928                    hard("/set mode -retain %s", umode);
929                    if (m != null && !m.equals(rm)) {
930                        hard("");
931                        showModeSettings(m);
932                    }
933                } else {
934                    showModeSettings(m);
935                }
936            }
937        }
938
939        void showModeSettings(Mode sm) {
940            hard("/set mode %s %s",
941                    sm.name, sm.commandFluff ? "-command" : "-quiet");
942            showPromptSettings(sm);
943            showFormatSettings(sm, null);
944            showTruncationSettings(sm);
945        }
946
947        void showFeedbackSetting() {
948            if (retainedCurrentMode != null) {
949                hard("/set feedback -retain %s", retainedCurrentMode.name);
950            }
951            if (mode != retainedCurrentMode) {
952                hard("/set feedback %s", mode.name);
953            }
954        }
955
956        // For /set prompt <mode> "<prompt>" "<continuation-prompt>"
957        boolean setPrompt() {
958            Mode m = nextMode();
959            String prompt = nextFormat();
960            String continuationPrompt = nextFormat();
961            checkOptionsAndRemainingInput();
962            if (valid && prompt == null) {
963                showPromptSettings(m);
964                return valid;
965            }
966            if (valid && m.readOnly) {
967                errorat("jshell.err.not.valid.with.predefined.mode", m.name);
968            } else if (continuationPrompt == null) {
969                errorat("jshell.err.continuation.prompt.required");
970            }
971            if (valid) {
972                m.setPrompts(prompt, continuationPrompt);
973            } else {
974                fluffmsg("jshell.msg.see", "/help /set prompt");
975            }
976            return valid;
977        }
978
979        /**
980         * Set mode. Create, changed, or delete a feedback mode. For @{code /set
981         * mode <mode> [<old-mode>] [-command|-quiet|-delete]}.
982         *
983         * @return true if successful
984         */
985        boolean setMode(Consumer<String> retainer) {
986            class SetMode {
987
988                final String umode;
989                final String omode;
990                final boolean commandOption;
991                final boolean quietOption;
992                final boolean deleteOption;
993                final boolean retainOption;
994
995                SetMode() {
996                    at.allowedOptions("-command", "-quiet", "-delete", "-retain");
997                    umode = nextModeIdentifier();
998                    omode = nextModeIdentifier();
999                    checkOptionsAndRemainingInput();
1000                    commandOption = at.hasOption("-command");
1001                    quietOption = at.hasOption("-quiet");
1002                    deleteOption = at.hasOption("-delete");
1003                    retainOption = at.hasOption("-retain");
1004                }
1005
1006                void delete() {
1007                    // Note: delete, for safety reasons, does NOT do name matching
1008                    if (commandOption || quietOption) {
1009                        errorat("jshell.err.conflicting.options");
1010                    } else if (!(retainOption ? retainedMap : modeMap).containsKey(umode)) {
1011                        // Cannot delete a mode that does not exist
1012                        errorat("jshell.err.mode.unknown", umode);
1013                    } else if (omode != null) {
1014                        // old mode is for creation
1015                        errorat("jshell.err.unexpected.at.end", omode);
1016                    } else if (mode.name.equals(umode)) {
1017                        // Cannot delete the current mode out from under us
1018                        errorat("jshell.err.cannot.delete.current.mode", umode);
1019                    } else if (retainOption && retainedCurrentMode != null &&
1020                             retainedCurrentMode.name.equals(umode)) {
1021                        // Cannot delete the retained mode or re-start will have an error
1022                        errorat("jshell.err.cannot.delete.retained.mode", umode);
1023                    } else {
1024                        Mode m = modeMap.get(umode);
1025                        if (m != null && m.readOnly) {
1026                            errorat("jshell.err.not.valid.with.predefined.mode", umode);
1027                        } else {
1028                            // Remove the mode
1029                            modeMap.remove(umode);
1030                            if (retainOption) {
1031                                // Remove the retained mode
1032                                retainedMap.remove(umode);
1033                                updateRetainedModes();
1034                            }
1035                        }
1036                    }
1037                }
1038
1039                void retain() {
1040                    if (commandOption || quietOption) {
1041                        errorat("jshell.err.conflicting.options");
1042                    } else if (omode != null) {
1043                        // old mode is for creation
1044                        errorat("jshell.err.unexpected.at.end", omode);
1045                    } else {
1046                        Mode m = modeMap.get(umode);
1047                        if (m == null) {
1048                            // can only retain existing modes
1049                            errorat("jshell.err.mode.unknown", umode);
1050                        } else if (m.readOnly) {
1051                            errorat("jshell.err.not.valid.with.predefined.mode", umode);
1052                        } else {
1053                            // Add to local cache of retained current encodings
1054                            retainedMap.put(m.name, m.encode());
1055                            updateRetainedModes();
1056                        }
1057                    }
1058                }
1059
1060                void updateRetainedModes() {
1061                    // Join all the retained encodings
1062                    String encoded = String.join(RECORD_SEPARATOR, retainedMap.values());
1063                    // Retain it
1064                    retainer.accept(encoded);
1065                }
1066
1067                void create() {
1068                    if (commandOption && quietOption) {
1069                        errorat("jshell.err.conflicting.options");
1070                    } else if (!commandOption && !quietOption) {
1071                        errorat("jshell.err.mode.creation");
1072                    } else if (modeMap.containsKey(umode)) {
1073                        // Mode already exists
1074                        errorat("jshell.err.mode.exists", umode);
1075                    } else {
1076                        Mode om = searchForMode(omode);
1077                        if (valid) {
1078                            // We are copying an existing mode and/or creating a
1079                            // brand-new mode -- in either case create from scratch
1080                            Mode m = (om != null)
1081                                    ? new Mode(umode, om)
1082                                    : new Mode(umode);
1083                            modeMap.put(umode, m);
1084                            fluffmsg("jshell.msg.feedback.new.mode", m.name);
1085                            m.setCommandFluff(commandOption);
1086                        }
1087                    }
1088                }
1089
1090                boolean set() {
1091                    if (valid && !commandOption && !quietOption && !deleteOption &&
1092                            omode == null && !retainOption) {
1093                        // Not a creation, deletion, or retain -- show mode(s)
1094                        showModeSettings(umode, "jshell.err.mode.creation");
1095                    } else if (valid && umode == null) {
1096                        errorat("jshell.err.missing.mode");
1097                    } else if (valid && deleteOption) {
1098                        delete();
1099                    } else if (valid && retainOption) {
1100                        retain();
1101                    } else if (valid) {
1102                        create();
1103                    }
1104                    if (!valid) {
1105                        fluffmsg("jshell.msg.see", "/help /set mode");
1106                    }
1107                    return valid;
1108                }
1109            }
1110            return new SetMode().set();
1111        }
1112
1113        // For /set format <mode> <field> "<format>" <selector>...
1114        boolean setFormat() {
1115            Mode m = nextMode();
1116            String field = toIdentifier(next(), "jshell.err.field.name");
1117            String format = nextFormat();
1118            if (valid && format == null) {
1119                if (field != null && m != null && !m.cases.containsKey(field)) {
1120                    errorat("jshell.err.field.name", field);
1121                } else {
1122                    showFormatSettings(m, field);
1123                }
1124            } else {
1125                installFormat(m, field, format, "/help /set format");
1126            }
1127            return valid;
1128        }
1129
1130        // For /set truncation <mode> <length> <selector>...
1131        boolean setTruncation() {
1132            Mode m = nextMode();
1133            String length = next();
1134            if (length == null) {
1135                showTruncationSettings(m);
1136            } else {
1137                try {
1138                    // Assure that integer format is correct
1139                    Integer.parseUnsignedInt(length);
1140                } catch (NumberFormatException ex) {
1141                    errorat("jshell.err.truncation.length.not.integer", length);
1142                }
1143                // install length into an internal format field
1144                installFormat(m, TRUNCATION_FIELD, length, "/help /set truncation");
1145            }
1146            return valid;
1147        }
1148
1149        // For /set feedback <mode>
1150        boolean setFeedback(Consumer<String> retainer) {
1151            String umode = next();
1152            checkOptionsAndRemainingInput();
1153            boolean retainOption = at.hasOption("-retain");
1154            if (valid && umode == null && !retainOption) {
1155                showFeedbackSetting();
1156                hard("");
1157                showFeedbackModes();
1158                return true;
1159            }
1160            if (valid) {
1161                Mode m = umode == null
1162                        ? mode
1163                        : searchForMode(toModeIdentifier(umode));
1164                if (valid && retainOption && !m.readOnly && !retainedMap.containsKey(m.name)) {
1165                    errorat("jshell.err.retained.feedback.mode.must.be.retained.or.predefined");
1166                }
1167                if (valid) {
1168                    if (umode != null) {
1169                        mode = m;
1170                        fluffmsg("jshell.msg.feedback.mode", mode.name);
1171                    }
1172                    if (retainOption) {
1173                        retainedCurrentMode = m;
1174                        retainer.accept(m.name);
1175                    }
1176                }
1177            }
1178            if (!valid) {
1179                fluffmsg("jshell.msg.see", "/help /set feedback");
1180                return false;
1181            }
1182            return true;
1183        }
1184
1185        boolean restoreEncodedModes(String allEncoded) {
1186            try {
1187                // Iterate over each record in each encoded mode
1188                Iterator<String> itr = encodedModeIterator(allEncoded);
1189                while (itr.hasNext()) {
1190                    // Reconstruct the encoded mode
1191                    Mode m = new Mode(itr);
1192                    modeMap.put(m.name, m);
1193                    // Continue to retain it a new retains occur
1194                    retainedMap.put(m.name, m.encode());
1195                }
1196                return true;
1197            } catch (Throwable exc) {
1198                // Catastrophic corruption -- clear map
1199                errorat("jshell.err.retained.mode.failure", exc);
1200                retainedMap.clear();
1201                return false;
1202            }
1203        }
1204
1205        Iterator<String> encodedModeIterator(String encoded) {
1206            String[] ms = encoded.split(RECORD_SEPARATOR);
1207            return Arrays.asList(ms).iterator();
1208        }
1209
1210        // install the format of a field under parsed selectors
1211        void installFormat(Mode m, String field, String format, String help) {
1212            String slRaw;
1213            List<SelectorList> slList = new ArrayList<>();
1214            while (valid && (slRaw = next()) != null) {
1215                SelectorList sl = new SelectorList();
1216                sl.parseSelectorList(slRaw);
1217                slList.add(sl);
1218            }
1219            checkOptionsAndRemainingInput();
1220            if (valid) {
1221                if (m.readOnly) {
1222                    errorat("jshell.err.not.valid.with.predefined.mode", m.name);
1223                } else if (slList.isEmpty()) {
1224                    // No selectors specified, then always the format
1225                    m.set(field, ALWAYS, format);
1226                } else {
1227                    // Set the format of the field for specified selector
1228                    slList.stream()
1229                            .forEach(sl -> m.set(field,
1230                            sl.cases.getSet(), sl.actions.getSet(), sl.whens.getSet(),
1231                            sl.resolves.getSet(), sl.unresolvedCounts.getSet(), sl.errorCounts.getSet(),
1232                            format));
1233                }
1234            } else {
1235                fluffmsg("jshell.msg.see", help);
1236            }
1237        }
1238
1239        void checkOptionsAndRemainingInput() {
1240            String junk = at.remainder();
1241            if (!junk.isEmpty()) {
1242                errorat("jshell.err.unexpected.at.end", junk);
1243            } else {
1244                String bad = at.badOptions();
1245                if (!bad.isEmpty()) {
1246                    errorat("jshell.err.unknown.option", bad);
1247                }
1248            }
1249        }
1250
1251        String next() {
1252            String s = at.next();
1253            if (s == null) {
1254                checkOptionsAndRemainingInput();
1255            }
1256            return s;
1257        }
1258
1259        /**
1260         * Check that the specified string is an identifier (Java identifier).
1261         * If null display the missing error. If it is not an identifier,
1262         * display the error.
1263         *
1264         * @param id the string to check, MUST be the most recently retrieved
1265         * token from 'at'.
1266         * @param missing null for no null error, otherwise the resource error to display if id is null
1267         * @param err the resource error to display if not an identifier
1268         * @return the identifier string, or null if null or not an identifier
1269         */
1270        private String toIdentifier(String id, String err) {
1271            if (!valid || id == null) {
1272                return null;
1273            }
1274            if (at.isQuoted() ||
1275                    !id.codePoints().allMatch(Character::isJavaIdentifierPart)) {
1276                errorat(err, id);
1277                return null;
1278            }
1279            return id;
1280        }
1281
1282        private String toModeIdentifier(String id) {
1283            return toIdentifier(id, "jshell.err.mode.name");
1284        }
1285
1286        private String nextModeIdentifier() {
1287            return toModeIdentifier(next());
1288        }
1289
1290        private Mode nextMode() {
1291            String umode = nextModeIdentifier();
1292            return searchForMode(umode);
1293        }
1294
1295        private Mode searchForMode(String umode) {
1296            return searchForMode(umode, null);
1297        }
1298
1299        private Mode searchForMode(String umode, String msg) {
1300            if (!valid || umode == null) {
1301                return null;
1302            }
1303            Mode m = modeMap.get(umode);
1304            if (m != null) {
1305                return m;
1306            }
1307            // Failing an exact match, go searching
1308            Mode[] matches = modeMap.entrySet().stream()
1309                    .filter(e -> e.getKey().startsWith(umode))
1310                    .map(Entry::getValue)
1311                    .toArray(Mode[]::new);
1312            if (matches.length == 1) {
1313                return matches[0];
1314            } else {
1315                if (msg != null) {
1316                    hardmsg(msg, "");
1317                }
1318                if (matches.length == 0) {
1319                    errorat("jshell.err.feedback.does.not.match.mode", umode);
1320                } else {
1321                    errorat("jshell.err.feedback.ambiguous.mode", umode);
1322                }
1323                if (showFluff()) {
1324                    showFeedbackModes();
1325                }
1326                return null;
1327            }
1328        }
1329
1330        void showFeedbackModes() {
1331            if (!retainedMap.isEmpty()) {
1332                hardmsg("jshell.msg.feedback.retained.mode.following");
1333                retainedMap.keySet().stream()
1334                        .sorted()
1335                        .forEach(mk -> hard("   %s", mk));
1336            }
1337            hardmsg("jshell.msg.feedback.mode.following");
1338            modeMap.keySet().stream()
1339                    .sorted()
1340                    .forEach(mk -> hard("   %s", mk));
1341        }
1342
1343        // Read and test if the format string is correctly
1344        private String nextFormat() {
1345            return toFormat(next());
1346        }
1347
1348        // Test if the format string is correctly
1349        private String toFormat(String format) {
1350            if (!valid || format == null) {
1351                return null;
1352            }
1353            if (!at.isQuoted()) {
1354                errorat("jshell.err.feedback.must.be.quoted", format);
1355               return null;
1356            }
1357            return format;
1358        }
1359
1360        // Convert to a quoted string
1361        private String toStringLiteral(String s) {
1362            StringBuilder sb = new StringBuilder();
1363            sb.append('"');
1364            final int length = s.length();
1365            for (int offset = 0; offset < length;) {
1366                final int codepoint = s.codePointAt(offset);
1367
1368                switch (codepoint) {
1369                    case '\b':
1370                        sb.append("\\b");
1371                        break;
1372                    case '\t':
1373                        sb.append("\\t");
1374                        break;
1375                    case '\n':
1376                        sb.append("\\n");
1377                        break;
1378                    case '\f':
1379                        sb.append("\\f");
1380                        break;
1381                    case '\r':
1382                        sb.append("\\r");
1383                        break;
1384                    case '\"':
1385                        sb.append("\\\"");
1386                        break;
1387                    case '\'':
1388                        sb.append("\\'");
1389                        break;
1390                    case '\\':
1391                        sb.append("\\\\");
1392                        break;
1393                    default:
1394                        if (codepoint < 040) {
1395                            sb.append(String.format("\\%o", codepoint));
1396                        } else {
1397                            sb.appendCodePoint(codepoint);
1398                        }
1399                        break;
1400                }
1401
1402                // do something with the codepoint
1403                offset += Character.charCount(codepoint);
1404
1405            }
1406            sb.append('"');
1407            return sb.toString();
1408        }
1409
1410        class SelectorList {
1411
1412            SelectorCollector<FormatCase> cases = new SelectorCollector<>(FormatCase.all);
1413            SelectorCollector<FormatAction> actions = new SelectorCollector<>(FormatAction.all);
1414            SelectorCollector<FormatWhen> whens = new SelectorCollector<>(FormatWhen.all);
1415            SelectorCollector<FormatResolve> resolves = new SelectorCollector<>(FormatResolve.all);
1416            SelectorCollector<FormatUnresolved> unresolvedCounts = new SelectorCollector<>(FormatUnresolved.all);
1417            SelectorCollector<FormatErrors> errorCounts = new SelectorCollector<>(FormatErrors.all);
1418
1419            final void parseSelectorList(String sl) {
1420                for (String s : sl.split("-")) {
1421                    SelectorCollector<?> lastCollector = null;
1422                    for (String as : s.split(",")) {
1423                        if (!as.isEmpty()) {
1424                            Selector<?> sel = selectorMap.get(as);
1425                            if (sel == null) {
1426                                errorat("jshell.err.feedback.not.a.valid.selector", as, s);
1427                                return;
1428                            }
1429                            SelectorCollector<?> collector = sel.collector(this);
1430                            if (lastCollector == null) {
1431                                if (!collector.isEmpty()) {
1432                                    errorat("jshell.err.feedback.multiple.sections", as, s);
1433                                    return;
1434                                }
1435                            } else if (collector != lastCollector) {
1436                                errorat("jshell.err.feedback.different.selector.kinds", as, s);
1437                                return;
1438                            }
1439                            collector.add(sel);
1440                            lastCollector = collector;
1441                        }
1442                    }
1443                }
1444            }
1445        }
1446    }
1447}
1448