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