Startup.java revision 3898:491ba4ffb03a
167204Sobrien/*
267204Sobrien * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
367204Sobrien * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
467204Sobrien *
567204Sobrien * This code is free software; you can redistribute it and/or modify it
667204Sobrien * under the terms of the GNU General Public License version 2 only, as
767204Sobrien * published by the Free Software Foundation.  Oracle designates this
867204Sobrien * particular file as subject to the "Classpath" exception as provided
967204Sobrien * by Oracle in the LICENSE file that accompanied this code.
1067204Sobrien *
1167204Sobrien * This code is distributed in the hope that it will be useful, but WITHOUT
1267204Sobrien * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
1367204Sobrien * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
1467204Sobrien * version 2 for more details (a copy is included in the LICENSE file that
1567204Sobrien * accompanied this code).
1667204Sobrien *
1767204Sobrien * You should have received a copy of the GNU General Public License version
1867204Sobrien * 2 along with this work; if not, write to the Free Software Foundation,
1967204Sobrien * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
2067204Sobrien *
2167204Sobrien * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
2267204Sobrien * or visit www.oracle.com if you need additional information or have any
2367204Sobrien * questions.
2467204Sobrien */
2567204Sobrien
2667204Sobrienpackage jdk.internal.jshell.tool;
2767204Sobrien
2867204Sobrienimport java.nio.file.AccessDeniedException;
2967204Sobrienimport java.nio.file.Files;
3067204Sobrienimport java.nio.file.NoSuchFileException;
3167204Sobrienimport java.time.LocalDateTime;
3267204Sobrienimport java.time.format.DateTimeFormatter;
3367204Sobrienimport java.time.format.FormatStyle;
3467204Sobrienimport java.util.ArrayList;
3567204Sobrienimport java.util.Collections;
3667204Sobrienimport java.util.List;
3767204Sobrienimport java.util.Objects;
3867204Sobrienimport static java.util.stream.Collectors.joining;
3967204Sobrienimport static java.util.stream.Collectors.toList;
4067204Sobrienimport static jdk.internal.jshell.tool.JShellTool.RECORD_SEPARATOR;
4167204Sobrienimport static jdk.internal.jshell.tool.JShellTool.getResource;
4267204Sobrienimport static jdk.internal.jshell.tool.JShellTool.readResource;
4367204Sobrienimport static jdk.internal.jshell.tool.JShellTool.toPathResolvingUserHome;
4467204Sobrien
4567204Sobrien/**
4667204Sobrien * Processing start-up "script" information.  The startup may consist of several
4767204Sobrien * entries, each of which may have been read from a user file or be a built-in
4867204Sobrien * resource.  The startup may also be empty ("-none"); Which is stored as the
4967204Sobrien * empty string different from unset (null).  Built-in resources come from
5067204Sobrien * resource files.  Startup is stored as named elements rather than concatenated
5167204Sobrien * text, for display purposes but also importantly so that when resources update
5267204Sobrien * with new releases the built-in will update.
5367204Sobrien * @author Robert Field
5467204Sobrien */
5567204Sobrienclass Startup {
5667204Sobrien
5767204Sobrien    // Store one entry in the start-up list
5867204Sobrien    private static class StartupEntry {
5967204Sobrien
6078877Sbenno        // is this a JShell built-in?
6178877Sbenno        private final boolean isBuiltIn;
6278877Sbenno
6367204Sobrien        // the file or resource name
64133862Smarius        private final String name;
6567204Sobrien
6667204Sobrien        // the commands/snippets as text
67105397Stmm        private final String content;
68105397Stmm
69105397Stmm        // for files, the date/time read in -- makes clear it is a snapshot
70105397Stmm        private final String timeStamp;
71105397Stmm
72105397Stmm        StartupEntry(boolean isBuiltIn, String name, String content) {
7367204Sobrien            this(isBuiltIn, name, content, "");
7467204Sobrien        }
7586557Stmm
7667204Sobrien        StartupEntry(boolean isBuiltIn, String name, String content, String timeStamp) {
7786557Stmm            this.isBuiltIn = isBuiltIn;
7886557Stmm            this.name = name;
7967204Sobrien            this.content = content;
80133862Smarius            this.timeStamp = timeStamp;
8178346Sbenno        }
8278346Sbenno
8378346Sbenno        // string form to store in storage (e.g. Preferences)
8478346Sbenno        String storedForm() {
8578346Sbenno            return (isBuiltIn ? "*" : "-") + RECORD_SEPARATOR +
86133862Smarius                    name + RECORD_SEPARATOR +
8767204Sobrien                    timeStamp + RECORD_SEPARATOR +
8867204Sobrien                    content + RECORD_SEPARATOR;
8967204Sobrien        }
9067204Sobrien
9167204Sobrien        // the content
9267204Sobrien        @Override
9378346Sbenno        public String toString() {
9478346Sbenno            return content;
9567204Sobrien        }
9667204Sobrien
9767204Sobrien        @Override
9867204Sobrien        public int hashCode() {
9967204Sobrien            int hash = 7;
10067204Sobrien            hash = 41 * hash + (this.isBuiltIn ? 1 : 0);
10167204Sobrien            hash = 41 * hash + Objects.hashCode(this.name);
10267204Sobrien            if (!isBuiltIn) {
10386557Stmm                hash = 41 * hash + Objects.hashCode(this.content);
10486557Stmm            }
10567204Sobrien            return hash;
10667204Sobrien        }
10768548Sbenno
10868548Sbenno        // built-ins match on name only.  Time stamp isn't considered
10967204Sobrien        @Override
11067204Sobrien        public boolean equals(Object o) {
11167204Sobrien            if (!(o instanceof StartupEntry)) {
11267204Sobrien                return false;
11367204Sobrien            }
11467204Sobrien            StartupEntry sue = (StartupEntry) o;
11567204Sobrien            return isBuiltIn == sue.isBuiltIn &&
11667204Sobrien                     name.equals(sue.name) &&
11767204Sobrien                     (isBuiltIn || content.equals(sue.content));
11867204Sobrien        }
11967204Sobrien    }
12067204Sobrien
12167204Sobrien    private static final String DEFAULT_STARTUP_NAME = "DEFAULT";
12267204Sobrien
12367204Sobrien    // cached DEFAULT start-up
12467204Sobrien    private static Startup defaultStartup = null;
12567204Sobrien
12667204Sobrien    // the list of entries
12768548Sbenno    private List<StartupEntry> entries;
12878346Sbenno
12978346Sbenno    // the concatenated content of the list of entries
13067204Sobrien    private String content;
131115973Sjake
132115973Sjake    // created only with factory methods (below)
13367204Sobrien    private Startup(List<StartupEntry> entries) {
13467204Sobrien        this.entries = entries;
13567204Sobrien        this.content = entries.stream()
13667204Sobrien                .map(sue -> sue.toString())
13767204Sobrien                .collect(joining());
13867204Sobrien    }
13967204Sobrien
14078877Sbenno    private Startup(StartupEntry entry) {
141105397Stmm        this(Collections.singletonList(entry));
14278877Sbenno    }
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