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 
26 package jdk.internal.jshell.tool;
27 
28 import java.nio.file.AccessDeniedException;
29 import java.nio.file.Files;
30 import java.nio.file.NoSuchFileException;
31 import java.time.LocalDateTime;
32 import java.time.format.DateTimeFormatter;
33 import java.time.format.FormatStyle;
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.List;
37 import java.util.Objects;
38 import static java.util.stream.Collectors.joining;
39 import static java.util.stream.Collectors.toList;
40 import static jdk.internal.jshell.tool.JShellTool.RECORD_SEPARATOR;
41 import static jdk.internal.jshell.tool.JShellTool.getResource;
42 import static jdk.internal.jshell.tool.JShellTool.readResource;
43 import 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  */
55 class 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 
StartupEntry(boolean isBuiltIn, String name, String content)72         StartupEntry(boolean isBuiltIn, String name, String content) {
73             this(isBuiltIn, name, content, "");
74         }
75 
StartupEntry(boolean isBuiltIn, String name, String content, String timeStamp)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)
storedForm()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
toString()93         public String toString() {
94             return content;
95         }
96 
97         @Override
hashCode()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
equals(Object o)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)
Startup(List<StartupEntry> entries)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 
Startup(StartupEntry entry)140     private Startup(StartupEntry entry) {
141         this(Collections.singletonList(entry));
142     }
143 
144     // retrieve the content
145     @Override
toString()146     public String toString() {
147         return content;
148     }
149 
150     @Override
hashCode()151     public int hashCode() {
152         return 9  + Objects.hashCode(this.entries);
153     }
154 
155     @Override
equals(Object o)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")?
isEmpty()162     boolean isEmpty() {
163         return entries.isEmpty();
164     }
165 
166     // is this the "-default" setting -- one entry which is DEFAULT
isDefault()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)
storedForm()178     String storedForm() {
179         return entries.stream()
180                 .map(sue -> sue.storedForm())
181                 .collect(joining());
182     }
183 
184     // show commands to re-create
show(boolean isRetained)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()
showDetail()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      */
unpack(String storedForm, MessageHandler mh)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      */
fromFileList(List<String> fns, String context, MessageHandler mh)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      */
readFile(String filename, String context, MessageHandler mh)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      */
noStartup()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      */
defaultStartup(MessageHandler mh)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