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