blob: da8b342e4f733675b9b181caa7294b506906be9e [file] [log] [blame]
/*
* Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.internal.jshell.tool;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static jdk.internal.jshell.tool.JShellTool.RECORD_SEPARATOR;
import static jdk.internal.jshell.tool.JShellTool.getResource;
import static jdk.internal.jshell.tool.JShellTool.readResource;
import static jdk.internal.jshell.tool.JShellTool.toPathResolvingUserHome;
/**
* Processing start-up "script" information. The startup may consist of several
* entries, each of which may have been read from a user file or be a built-in
* resource. The startup may also be empty ("-none"); Which is stored as the
* empty string different from unset (null). Built-in resources come from
* resource files. Startup is stored as named elements rather than concatenated
* text, for display purposes but also importantly so that when resources update
* with new releases the built-in will update.
* @author Robert Field
*/
class Startup {
// Store one entry in the start-up list
private static class StartupEntry {
// is this a JShell built-in?
private final boolean isBuiltIn;
// the file or resource name
private final String name;
// the commands/snippets as text
private final String content;
// for files, the date/time read in -- makes clear it is a snapshot
private final String timeStamp;
StartupEntry(boolean isBuiltIn, String name, String content) {
this(isBuiltIn, name, content, "");
}
StartupEntry(boolean isBuiltIn, String name, String content, String timeStamp) {
this.isBuiltIn = isBuiltIn;
this.name = name;
this.content = content;
this.timeStamp = timeStamp;
}
// string form to store in storage (e.g. Preferences)
String storedForm() {
return (isBuiltIn ? "*" : "-") + RECORD_SEPARATOR +
name + RECORD_SEPARATOR +
timeStamp + RECORD_SEPARATOR +
content + RECORD_SEPARATOR;
}
// the content
@Override
public String toString() {
return content;
}
@Override
public int hashCode() {
int hash = 7;
hash = 41 * hash + (this.isBuiltIn ? 1 : 0);
hash = 41 * hash + Objects.hashCode(this.name);
if (!isBuiltIn) {
hash = 41 * hash + Objects.hashCode(this.content);
}
return hash;
}
// built-ins match on name only. Time stamp isn't considered
@Override
public boolean equals(Object o) {
if (!(o instanceof StartupEntry)) {
return false;
}
StartupEntry sue = (StartupEntry) o;
return isBuiltIn == sue.isBuiltIn &&
name.equals(sue.name) &&
(isBuiltIn || content.equals(sue.content));
}
}
private static final String DEFAULT_STARTUP_NAME = "DEFAULT";
// cached DEFAULT start-up
private static Startup defaultStartup = null;
// the list of entries
private List<StartupEntry> entries;
// the concatenated content of the list of entries
private String content;
// created only with factory methods (below)
private Startup(List<StartupEntry> entries) {
this.entries = entries;
this.content = entries.stream()
.map(sue -> sue.toString())
.collect(joining());
}
private Startup(StartupEntry entry) {
this(Collections.singletonList(entry));
}
// retrieve the content
@Override
public String toString() {
return content;
}
@Override
public int hashCode() {
return 9 + Objects.hashCode(this.entries);
}
@Override
public boolean equals(Object o) {
return (o instanceof Startup)
&& entries.equals(((Startup) o).entries);
}
// are there no entries ("-none")?
boolean isEmpty() {
return entries.isEmpty();
}
// is this the "-default" setting -- one entry which is DEFAULT
boolean isDefault() {
if (entries.size() == 1) {
StartupEntry sue = entries.get(0);
if (sue.isBuiltIn && sue.name.equals(DEFAULT_STARTUP_NAME)) {
return true;
}
}
return false;
}
// string form to store in storage (e.g. Preferences)
String storedForm() {
return entries.stream()
.map(sue -> sue.storedForm())
.collect(joining());
}
// show commands to re-create
String show(boolean isRetained) {
String cmd = "/set start " + (isRetained ? "-retain " : "");
if (isDefault()) {
return cmd + "-default\n";
} else if (isEmpty()) {
return cmd + "-none\n";
} else {
return entries.stream()
.map(sue -> sue.name)
.collect(joining(" ", cmd, "\n"));
}
}
// show corresponding contents for show()
String showDetail() {
if (isDefault() || isEmpty()) {
return "";
} else {
return entries.stream()
.map(sue -> "---- " + sue.name
+ (sue.timeStamp.isEmpty()
? ""
: " @ " + sue.timeStamp)
+ " ----\n" + sue.content)
.collect(joining());
}
}
/**
* Factory method: Unpack from stored form.
*
* @param storedForm the Startup in the form as stored on persistent
* storage (e.g. Preferences)
* @param mh handler for error messages
* @return Startup, or default startup when error (message has been printed)
*/
static Startup unpack(String storedForm, MessageHandler mh) {
if (storedForm != null) {
if (storedForm.isEmpty()) {
return noStartup();
}
try {
String[] all = storedForm.split(RECORD_SEPARATOR);
if (all.length == 1) {
// legacy (content only)
return new Startup(new StartupEntry(false, "user.jsh", storedForm));
} else if (all.length % 4 == 0) {
List<StartupEntry> e = new ArrayList<>(all.length / 4);
for (int i = 0; i < all.length; i += 4) {
final boolean isBuiltIn;
switch (all[i]) {
case "*":
isBuiltIn = true;
break;
case "-":
isBuiltIn = false;
break;
default:
throw new IllegalArgumentException("Unexpected StartupEntry kind: " + all[i]);
}
String name = all[i + 1];
String timeStamp = all[i + 2];
String content = all[i + 3];
if (isBuiltIn) {
// update to current definition, use stored if removed/error
String resource = getResource(name);
if (resource != null) {
content = resource;
}
}
e.add(new StartupEntry(isBuiltIn, name, content, timeStamp));
}
return new Startup(e);
} else {
throw new IllegalArgumentException("Unexpected StartupEntry entry count: " + all.length);
}
} catch (Exception ex) {
mh.errormsg("jshell.err.corrupted.stored.startup", ex.getMessage());
}
}
return defaultStartup(mh);
}
/**
* Factory method: Read Startup from a list of external files or resources.
*
* @param fns list of file/resource names to access
* @param context printable non-natural language context for errors
* @param mh handler for error messages
* @return files as Startup, or null when error (message has been printed)
*/
static Startup fromFileList(List<String> fns, String context, MessageHandler mh) {
List<StartupEntry> entries = fns.stream()
.map(fn -> readFile(fn, context, mh))
.collect(toList());
if (entries.stream().anyMatch(sue -> sue == null)) {
return null;
}
return new Startup(entries);
}
/**
* Read a external file or a resource.
*
* @param filename file/resource to access
* @param context printable non-natural language context for errors
* @param mh handler for error messages
* @return file as startup entry, or null when error (message has been printed)
*/
private static StartupEntry readFile(String filename, String context, MessageHandler mh) {
if (filename != null) {
try {
byte[] encoded = Files.readAllBytes(toPathResolvingUserHome(filename));
return new StartupEntry(false, filename, new String(encoded),
LocalDateTime.now().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)));
} catch (AccessDeniedException e) {
mh.errormsg("jshell.err.file.not.accessible", context, filename, e.getMessage());
} catch (NoSuchFileException e) {
String resource = getResource(filename);
if (resource != null) {
// Not found as file, but found as resource
return new StartupEntry(true, filename, resource);
}
mh.errormsg("jshell.err.file.not.found", context, filename);
} catch (Exception e) {
mh.errormsg("jshell.err.file.exception", context, filename, e);
}
} else {
mh.errormsg("jshell.err.file.filename", context);
}
return null;
}
/**
* Factory method: The empty Startup ("-none").
*
* @return the empty Startup
*/
static Startup noStartup() {
return new Startup(Collections.emptyList());
}
/**
* Factory method: The default Startup ("-default.").
*
* @param mh handler for error messages
* @return The default Startup, or empty startup when error (message has been printed)
*/
static Startup defaultStartup(MessageHandler mh) {
if (defaultStartup != null) {
return defaultStartup;
}
try {
String content = readResource(DEFAULT_STARTUP_NAME);
return defaultStartup = new Startup(
new StartupEntry(true, DEFAULT_STARTUP_NAME, content));
} catch (AccessDeniedException e) {
mh.errormsg("jshell.err.file.not.accessible", "jshell", DEFAULT_STARTUP_NAME, e.getMessage());
} catch (NoSuchFileException e) {
mh.errormsg("jshell.err.file.not.found", "jshell", DEFAULT_STARTUP_NAME);
} catch (Exception e) {
mh.errormsg("jshell.err.file.exception", "jshell", DEFAULT_STARTUP_NAME, e);
}
return defaultStartup = noStartup();
}
}