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.nio.file.AccessDeniedException; 29import java.nio.file.Files; 30import java.nio.file.NoSuchFileException; 31import java.time.LocalDateTime; 32import java.time.format.DateTimeFormatter; 33import java.time.format.FormatStyle; 34import java.util.ArrayList; 35import java.util.Collections; 36import java.util.List; 37import java.util.Objects; 38import static java.util.stream.Collectors.joining; 39import static java.util.stream.Collectors.toList; 40import static jdk.internal.jshell.tool.JShellTool.RECORD_SEPARATOR; 41import static jdk.internal.jshell.tool.JShellTool.getResource; 42import static jdk.internal.jshell.tool.JShellTool.readResource; 43import static jdk.internal.jshell.tool.JShellTool.toPathResolvingUserHome; 44 45/** 46 * Processing start-up "script" information. The startup may consist of several 47 * entries, each of which may have been read from a user file or be a built-in 48 * resource. The startup may also be empty ("-none"); Which is stored as the 49 * empty string different from unset (null). Built-in resources come from 50 * resource files. Startup is stored as named elements rather than concatenated 51 * text, for display purposes but also importantly so that when resources update 52 * with new releases the built-in will update. 53 * @author Robert Field 54 */ 55class Startup { 56 57 // Store one entry in the start-up list 58 private static class StartupEntry { 59 60 // is this a JShell built-in? 61 private final boolean isBuiltIn; 62 63 // the file or resource name 64 private final String name; 65 66 // the commands/snippets as text 67 private final String content; 68 69 // for files, the date/time read in -- makes clear it is a snapshot 70 private final String timeStamp; 71 72 StartupEntry(boolean isBuiltIn, String name, String content) { 73 this(isBuiltIn, name, content, ""); 74 } 75 76 StartupEntry(boolean isBuiltIn, String name, String content, String timeStamp) { 77 this.isBuiltIn = isBuiltIn; 78 this.name = name; 79 this.content = content; 80 this.timeStamp = timeStamp; 81 } 82 83 // string form to store in storage (e.g. Preferences) 84 String storedForm() { 85 return (isBuiltIn ? "*" : "-") + RECORD_SEPARATOR + 86 name + RECORD_SEPARATOR + 87 timeStamp + RECORD_SEPARATOR + 88 content + RECORD_SEPARATOR; 89 } 90 91 // the content 92 @Override 93 public String toString() { 94 return content; 95 } 96 97 @Override 98 public int hashCode() { 99 int hash = 7; 100 hash = 41 * hash + (this.isBuiltIn ? 1 : 0); 101 hash = 41 * hash + Objects.hashCode(this.name); 102 if (!isBuiltIn) { 103 hash = 41 * hash + Objects.hashCode(this.content); 104 } 105 return hash; 106 } 107 108 // built-ins match on name only. Time stamp isn't considered 109 @Override 110 public boolean equals(Object o) { 111 if (!(o instanceof StartupEntry)) { 112 return false; 113 } 114 StartupEntry sue = (StartupEntry) o; 115 return isBuiltIn == sue.isBuiltIn && 116 name.equals(sue.name) && 117 (isBuiltIn || content.equals(sue.content)); 118 } 119 } 120 121 private static final String DEFAULT_STARTUP_NAME = "DEFAULT"; 122 123 // cached DEFAULT start-up 124 private static Startup defaultStartup = null; 125 126 // the list of entries 127 private List<StartupEntry> entries; 128 129 // the concatenated content of the list of entries 130 private String content; 131 132 // created only with factory methods (below) 133 private Startup(List<StartupEntry> entries) { 134 this.entries = entries; 135 this.content = entries.stream() 136 .map(sue -> sue.toString()) 137 .collect(joining()); 138 } 139 140 private Startup(StartupEntry entry) { 141 this(Collections.singletonList(entry)); 142 } 143 144 // retrieve the content 145 @Override 146 public String toString() { 147 return content; 148 } 149 150 @Override 151 public int hashCode() { 152 return 9 + Objects.hashCode(this.entries); 153 } 154 155 @Override 156 public boolean equals(Object o) { 157 return (o instanceof Startup) 158 && entries.equals(((Startup) o).entries); 159 } 160 161 // are there no entries ("-none")? 162 boolean isEmpty() { 163 return entries.isEmpty(); 164 } 165 166 // is this the "-default" setting -- one entry which is DEFAULT 167 boolean isDefault() { 168 if (entries.size() == 1) { 169 StartupEntry sue = entries.get(0); 170 if (sue.isBuiltIn && sue.name.equals(DEFAULT_STARTUP_NAME)) { 171 return true; 172 } 173 } 174 return false; 175 } 176 177 // string form to store in storage (e.g. Preferences) 178 String storedForm() { 179 return entries.stream() 180 .map(sue -> sue.storedForm()) 181 .collect(joining()); 182 } 183 184 // show commands to re-create 185 String show(boolean isRetained) { 186 String cmd = "/set start " + (isRetained ? "-retain " : ""); 187 if (isDefault()) { 188 return cmd + "-default\n"; 189 } else if (isEmpty()) { 190 return cmd + "-none\n"; 191 } else { 192 return entries.stream() 193 .map(sue -> sue.name) 194 .collect(joining(" ", cmd, "\n")); 195 } 196 } 197 198 // show corresponding contents for show() 199 String showDetail() { 200 if (isDefault() || isEmpty()) { 201 return ""; 202 } else { 203 return entries.stream() 204 .map(sue -> "---- " + sue.name 205 + (sue.timeStamp.isEmpty() 206 ? "" 207 : " @ " + sue.timeStamp) 208 + " ----\n" + sue.content) 209 .collect(joining()); 210 } 211 } 212 213 /** 214 * Factory method: Unpack from stored form. 215 * 216 * @param storedForm the Startup in the form as stored on persistent 217 * storage (e.g. Preferences) 218 * @param mh handler for error messages 219 * @return Startup, or default startup when error (message has been printed) 220 */ 221 static Startup unpack(String storedForm, MessageHandler mh) { 222 if (storedForm != null) { 223 if (storedForm.isEmpty()) { 224 return noStartup(); 225 } 226 try { 227 String[] all = storedForm.split(RECORD_SEPARATOR); 228 if (all.length == 1) { 229 // legacy (content only) 230 return new Startup(new StartupEntry(false, "user.jsh", storedForm)); 231 } else if (all.length % 4 == 0) { 232 List<StartupEntry> e = new ArrayList<>(all.length / 4); 233 for (int i = 0; i < all.length; i += 4) { 234 final boolean isBuiltIn; 235 switch (all[i]) { 236 case "*": 237 isBuiltIn = true; 238 break; 239 case "-": 240 isBuiltIn = false; 241 break; 242 default: 243 throw new IllegalArgumentException("Unexpected StartupEntry kind: " + all[i]); 244 } 245 String name = all[i + 1]; 246 String timeStamp = all[i + 2]; 247 String content = all[i + 3]; 248 if (isBuiltIn) { 249 // update to current definition, use stored if removed/error 250 String resource = getResource(name); 251 if (resource != null) { 252 content = resource; 253 } 254 } 255 e.add(new StartupEntry(isBuiltIn, name, content, timeStamp)); 256 } 257 return new Startup(e); 258 } else { 259 throw new IllegalArgumentException("Unexpected StartupEntry entry count: " + all.length); 260 } 261 } catch (Exception ex) { 262 mh.errormsg("jshell.err.corrupted.stored.startup", ex.getMessage()); 263 } 264 } 265 return defaultStartup(mh); 266 } 267 268 /** 269 * Factory method: Read Startup from a list of external files or resources. 270 * 271 * @param fns list of file/resource names to access 272 * @param context printable non-natural language context for errors 273 * @param mh handler for error messages 274 * @return files as Startup, or null when error (message has been printed) 275 */ 276 static Startup fromFileList(List<String> fns, String context, MessageHandler mh) { 277 List<StartupEntry> entries = fns.stream() 278 .map(fn -> readFile(fn, context, mh)) 279 .collect(toList()); 280 if (entries.stream().anyMatch(sue -> sue == null)) { 281 return null; 282 } 283 return new Startup(entries); 284 } 285 286 /** 287 * Read a external file or a resource. 288 * 289 * @param filename file/resource to access 290 * @param context printable non-natural language context for errors 291 * @param mh handler for error messages 292 * @return file as startup entry, or null when error (message has been printed) 293 */ 294 private static StartupEntry readFile(String filename, String context, MessageHandler mh) { 295 if (filename != null) { 296 try { 297 byte[] encoded = Files.readAllBytes(toPathResolvingUserHome(filename)); 298 return new StartupEntry(false, filename, new String(encoded), 299 LocalDateTime.now().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM))); 300 } catch (AccessDeniedException e) { 301 mh.errormsg("jshell.err.file.not.accessible", context, filename, e.getMessage()); 302 } catch (NoSuchFileException e) { 303 String resource = getResource(filename); 304 if (resource != null) { 305 // Not found as file, but found as resource 306 return new StartupEntry(true, filename, resource); 307 } 308 mh.errormsg("jshell.err.file.not.found", context, filename); 309 } catch (Exception e) { 310 mh.errormsg("jshell.err.file.exception", context, filename, e); 311 } 312 } else { 313 mh.errormsg("jshell.err.file.filename", context); 314 } 315 return null; 316 317 } 318 319 /** 320 * Factory method: The empty Startup ("-none"). 321 * 322 * @return the empty Startup 323 */ 324 static Startup noStartup() { 325 return new Startup(Collections.emptyList()); 326 } 327 328 /** 329 * Factory method: The default Startup ("-default."). 330 * 331 * @param mh handler for error messages 332 * @return The default Startup, or empty startup when error (message has been printed) 333 */ 334 static Startup defaultStartup(MessageHandler mh) { 335 if (defaultStartup != null) { 336 return defaultStartup; 337 } 338 try { 339 String content = readResource(DEFAULT_STARTUP_NAME); 340 return defaultStartup = new Startup( 341 new StartupEntry(true, DEFAULT_STARTUP_NAME, content)); 342 } catch (AccessDeniedException e) { 343 mh.errormsg("jshell.err.file.not.accessible", "jshell", DEFAULT_STARTUP_NAME, e.getMessage()); 344 } catch (NoSuchFileException e) { 345 mh.errormsg("jshell.err.file.not.found", "jshell", DEFAULT_STARTUP_NAME); 346 } catch (Exception e) { 347 mh.errormsg("jshell.err.file.exception", "jshell", DEFAULT_STARTUP_NAME, e); 348 } 349 return defaultStartup = noStartup(); 350 } 351 352} 353