dev.jbang.util.Util Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jbang-cli Show documentation
Show all versions of jbang-cli Show documentation
JBang Command Line Interface
The newest version!
package dev.jbang.util;
import java.awt.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.swing.*;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import dev.jbang.Cache;
import dev.jbang.Configuration;
import dev.jbang.Settings;
import dev.jbang.catalog.Catalog;
import dev.jbang.cli.BaseCommand;
import dev.jbang.cli.ExitException;
import dev.jbang.dependencies.DependencyResolver;
import dev.jbang.dependencies.DependencyUtil;
import dev.jbang.dependencies.ModularClassPath;
import dev.jbang.source.Source;
public class Util {
public static final String JBANG_JDK_VENDOR = "JBANG_JDK_VENDOR";
public static final String JBANG_RUNTIME_SHELL = "JBANG_RUNTIME_SHELL";
public static final String JBANG_STDIN_NOTTY = "JBANG_STDIN_NOTTY";
public static final String JBANG_AUTH_BASIC_USERNAME = "JBANG_AUTH_BASIC_USERNAME";
public static final String JBANG_AUTH_BASIC_PASSWORD = "JBANG_AUTH_BASIC_PASSWORD";
private static final String JBANG_DOWNLOAD_SOURCES = "JBANG_DOWNLOAD_SOURCES";
public static final Pattern patternMainMethod = Pattern.compile(
"^.*(public\\s+static|static\\s+public)\\s+void\\s+main\\s*\\(.*|void\\s+main\\s*\\(\\)",
Pattern.MULTILINE);
public static final Pattern mainClassMethod = Pattern.compile(
"(?<=\\n|\\A)(?:public\\s)\\s*(class)\\s*([^\\n\\s]*)");
public static final Pattern patternFQCN = Pattern.compile(
"^([a-z][a-z0-9]*\\.)*[a-zA-Z][a-zA-Z0-9_]*$");
public static final Pattern patternModuleId = Pattern.compile(
"^[a-z][a-z0-9]*(\\.[a-z][a-z0-9]*)*$");
private static final Pattern subUrlPattern = Pattern.compile(
"^(%?%https?://.+$)|(%?%\\{[a-z]+:[^}]+})");
private static boolean verbose;
private static boolean quiet;
private static boolean offline;
private static boolean fresh;
private static boolean ignoreTransitiveRepositories;
private static boolean preview;
private static Path cwd;
private static Boolean downloadSources;
private static Instant startTime = Instant.now();
public static void setVerbose(boolean verbose) {
Util.verbose = verbose;
if (verbose) {
setQuiet(false);
}
}
public static boolean isVerbose() {
return verbose;
}
public static void setQuiet(boolean quiet) {
Util.quiet = quiet;
if (quiet) {
setVerbose(false);
}
}
public static boolean isQuiet() {
return quiet;
}
public static void setOffline(boolean offline) {
Util.offline = offline;
if (offline) {
setFresh(false);
}
}
public static void setIgnoreTransitiveRepositories(boolean ignoreTransitiveRepositories) {
Util.ignoreTransitiveRepositories = ignoreTransitiveRepositories;
}
public static void setDownloadSources(boolean flag) {
downloadSources = flag;
}
public static boolean downloadSources() {
if (downloadSources == null) {
downloadSources = Boolean.valueOf(System.getenv(JBANG_DOWNLOAD_SOURCES));
}
return downloadSources;
}
public static boolean isOffline() {
return offline;
}
public static boolean isIgnoreTransitiveRepositories() {
return ignoreTransitiveRepositories;
}
public static void setFresh(boolean fresh) {
Util.fresh = fresh;
if (fresh) {
setOffline(false);
}
}
public static boolean isPreview() {
return preview;
}
public static void setPreview(boolean preview) {
Util.preview = preview;
}
public static boolean isFresh() {
return fresh;
}
public static Path getCwd() {
return cwd != null ? cwd : Paths.get("").toAbsolutePath();
}
public static void setCwd(Path cwd) {
Util.cwd = cwd.toAbsolutePath().normalize();
}
/**
* Runs the given code with the global fresh
variable set to
* true
, thereby forcing that all requests to remote resources are
* actually performed and any previously cached documents are updated to their
* latest versions.
*/
public static T freshly(Callable func) {
boolean oldFresh = isFresh();
setFresh(true);
try {
return func.call();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
setFresh(oldFresh);
}
}
/**
* Runs the given code with the "cache-evict" configuration key set to a very
* low value. This forces all requests to remote resources to be performed.
* Cached resources will only be updated if the remote documents have actually
* been updated.
*/
public static T withCacheEvict(Callable func) {
return withCacheEvict("5", func);
}
public static T withCacheEvict(String val, Callable func) {
return withConfig(Settings.CONFIG_CACHE_EVICT, val, func);
}
public static T withConfig(String key, String value, Callable func) {
Configuration cfg = Configuration.create(Configuration.instance());
cfg.put(key, value);
return withConfig(cfg, func);
}
public static T withConfig(Configuration cfg, Callable func) {
Configuration oldCfg = Configuration.instance();
Configuration.instance(cfg);
try {
return func.call();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
Configuration.instance(oldCfg);
}
}
public static String kebab2camel(String name) {
if (name.contains("-")) { // xyz-plug becomes XyzPlug
return Arrays .stream(name.split("-"))
.map(s -> Character.toUpperCase(s.charAt(0)) + s.substring(1).toLowerCase())
.collect(Collectors.joining());
} else {
return name; // xyz stays xyz
}
}
public static String getBaseName(String fileName) {
String ext = extension(fileName);
if (ext.isEmpty()) {
return kebab2camel(fileName);
} else {
return base(fileName);
}
}
/**
* Returns the name without extension. Will return the name itself if it has no
* extension
*
* @param name A file name
* @return A name without extension
*/
public static String base(String name) {
int p = name.lastIndexOf('.');
return p > 0 ? name.substring(0, p) : name;
}
/**
* Returns the name without a valid source file extension. Will return the name
* itself if it has no extension or if the extension is not a valid source file
* extension.
*
* @param name A file name
* @return A name without a source file extension
*/
public static String sourceBase(String name) {
for (String extension : Source.Type.extensions()) {
if (name.endsWith("." + extension)) {
return base(name);
}
}
return name;
}
/**
* Returns the extension of the given file name. The extension will not include
* the dot as part of the result. Returns an empty string if the name has no
* extension.
*
* @param name A file name
* @return An extension or an empty string
*/
public static String extension(String name) {
int p = name.lastIndexOf('.');
return p > 0 ? name.substring(p + 1) : "";
}
static public boolean isPattern(String pattern) {
return pattern.contains("?") || pattern.contains("*");
}
/**
* Explodes filePattern found in baseDir returning list of relative Path names.
* If the filePattern refers to an existing folder the filePattern will be
* treated as if it ended in "/**".
*/
public static List explode(String source, Path baseDir, String filePattern) {
if (source != null && isURL(source)) {
// if url then just return it back for others to resolve.
// TODO: technically this is really where it should get resolved!
if (isPattern(filePattern)) {
warnMsg("Pattern " + filePattern + " used while using URL to run; this could result in errors.");
return Collections.emptyList();
} else {
return Collections.singletonList(filePattern);
}
} else if (isURL(filePattern)) {
return Collections.singletonList(filePattern);
}
if (!isPattern(filePattern)) {
if (!Catalog.isValidCatalogReference(filePattern)
&& isValidPath(filePattern) && Files.isDirectory(baseDir.resolve(filePattern))) {
// The filePattern refers to a folder, so let's add "/**"
if (!filePattern.endsWith("/") && !filePattern.endsWith(File.separator)) {
filePattern = filePattern + "/";
}
filePattern = filePattern + "**";
} else {
// not a pattern and not a folder thus just as well return path directly
return Collections.singletonList(filePattern);
}
}
// it is a non-url let's try to locate it
final Path bd;
final boolean useAbsPath;
Path base = basePathWithoutPattern(filePattern);
String fp;
if (base.isAbsolute()) {
bd = base;
fp = filePattern.substring(bd.toString().length() + 1);
useAbsPath = true;
} else {
bd = baseDir.resolve(base);
fp = base.toString().isEmpty() ? filePattern : filePattern.substring(base.toString().length() + 1);
useAbsPath = false;
}
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + fp);
List results = new ArrayList<>();
FileVisitor matcherVisitor = new SimpleFileVisitor() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attribs) {
Path relpath = bd.relativize(file);
if (matcher.matches(relpath)) {
// to avoid windows fail.
if (file.toFile().exists()) {
Path p = useAbsPath ? file : base.resolve(relpath);
if (isWindows()) {
results.add(p.toString().replace("\\", "/"));
} else {
results.add(p.toString());
}
} else {
verboseMsg("Warning: " + relpath + " matches but does not exist!");
}
}
return FileVisitResult.CONTINUE;
}
};
try {
Files.walkFileTree(bd, matcherVisitor);
} catch (IOException e) {
throw new ExitException(BaseCommand.EXIT_INTERNAL_ERROR,
"Problem looking for " + fp + " in " + bd + ": " + e, e);
}
return results;
}
public static Path basePathWithoutPattern(String path) {
int p1 = path.indexOf('?');
int p2 = path.indexOf('*');
int pp = p1 < 0 ? p2 : (p2 < 0 ? p1 : Math.min(p1, p2));
if (pp >= 0) {
String npath = isWindows() ? path.replace('\\', '/') : path;
int ps = npath.lastIndexOf('/', pp);
if (ps >= 0) {
return Paths.get(path.substring(0, ps + 1));
} else {
return Paths.get("");
}
} else {
return Paths.get(path);
}
}
/**
* @param name script name
* @return camel case of kebab string if name does not end with .java or .jsh
*/
public static String unkebabify(String name) {
if (name.endsWith(".sh")) {
name = name.substring(0, name.length() - 3);
}
boolean valid = false;
for (String extension : Source.Type.extensions()) {
valid |= name.endsWith("." + extension);
}
if (!valid) {
name = kebab2camel(name) + ".java";
}
return name;
}
public enum OS {
linux, alpine_linux, mac, windows, aix, unknown
}
public enum Arch {
x32, x64, aarch64, arm, arm64, ppc64, ppc64le, s390x, riscv64, unknown
}
public enum Shell {
bash, cmd, powershell
}
static public void verboseMsg(String msg) {
if (isVerbose()) {
System.err.print(getMsgHeader());
System.err.println(msg);
}
}
static public void verboseMsg(String msg, Throwable e) {
if (isVerbose()) {
System.err.print(getMsgHeader());
System.err.println(msg);
e.printStackTrace();
}
}
static public void infoMsg(String msg) {
if (!isQuiet()) {
System.err.print(getMsgHeader());
System.err.println(msg);
}
}
static public void warnMsg(String msg) {
if (!isQuiet()) {
System.err.print(getMsgHeader());
System.err.print("[WARN] ");
System.err.println(msg);
}
}
static public void errorMsg(String msg) {
System.err.print(getMsgHeader());
System.err.print("[ERROR] ");
System.err.println(msg);
}
static public void errorMsg(String msg, Throwable e) {
System.err.print(getMsgHeader());
System.err.print("[ERROR] ");
if (msg != null) {
System.err.println(msg);
} else if (e.getMessage() != null) {
System.err.println(e.getMessage());
} else {
System.err.println(e.getClass().toGenericString());
}
if (isVerbose()) {
e.printStackTrace();
} else {
if (msg != null) {
infoMsg(e.getMessage());
}
infoMsg("Run with --verbose for more details. The --verbose must be placed before the jbang command. I.e. jbang --verbose run [...]");
}
}
static public String getMsgHeader() {
if (isVerbose()) {
Duration d = Duration.between(startTime, Instant.now());
long s = d.getSeconds();
long n = d.minus(s, ChronoUnit.SECONDS).toMillis();
return String.format("[jbang] [%d:%03d] ", s, n);
} else {
return "[jbang] ";
}
}
static public void quit(int status) {
System.out.print(status > 0 ? "true" : "false");
throw new ExitException(status);
}
static public String readFileContent(Path file) {
try {
return readString(file);
} catch (IOException e) {
throw new ExitException(BaseCommand.EXIT_UNEXPECTED_STATE, "Could not read content for " + file, e);
}
}
/**
* Java 8 approximate version of Java 11 Files.readString()
**/
static public String readString(Path toPath) throws IOException {
return new String(Files.readAllBytes(toPath));
}
/**
* Java 8 approximate version of Java 11 Files.writeString()
**/
static public void writeString(Path toPath, String scriptText) throws IOException {
Files.write(toPath, scriptText.getBytes());
}
private static final Pattern mainClassPattern = Pattern.compile(
"(?sm)class *(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*) .*static void main");
private static final Pattern publicClassPattern = Pattern.compile(
"(?sm)public class *(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)");
public static OS getOS() {
String os = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).replaceAll("[^a-z0-9]+", "");
if (os.startsWith("mac") || os.startsWith("osx")) {
return OS.mac;
} else if (os.startsWith("linux")) {
if (Files.exists(Paths.get("/etc/alpine-release"))) {
return OS.alpine_linux;
} else {
return OS.linux;
}
} else if (os.startsWith("win")) {
return OS.windows;
} else if (os.startsWith("aix")) {
return OS.aix;
} else {
verboseMsg("Unknown OS: " + os);
return OS.unknown;
}
}
public static Arch getArch() {
String arch = System.getProperty("os.arch").toLowerCase(Locale.ENGLISH).replaceAll("[^a-z0-9]+", "");
if (arch.matches("^(x8664|amd64|ia32e|em64t|x64)$")) {
return Arch.x64;
} else if (arch.matches("^(x8632|x86|i[3-6]86|ia32|x32)$")) {
return Arch.x32;
} else if (arch.matches("^(aarch64)$")) {
return Arch.aarch64;
} else if (arch.matches("^(arm)$")) {
return Arch.arm;
} else if (arch.matches("^(ppc64)$")) {
return Arch.ppc64;
} else if (arch.matches("^(ppc64le)$")) {
return Arch.ppc64le;
} else if (arch.matches("^(s390x)$")) {
return Arch.s390x;
} else if (arch.matches("^(arm64)$")) {
return Arch.arm64;
} else if (arch.matches("^(riscv64)$")) {
return Arch.riscv64;
} else {
verboseMsg("Unknown Arch: " + arch);
return Arch.unknown;
}
}
public static boolean isWindows() {
return getOS() == OS.windows;
}
public static Shell getShell() {
try {
return Shell.valueOf(System.getenv(JBANG_RUNTIME_SHELL));
} catch (IllegalArgumentException | NullPointerException ex) {
// We'll just hope for the best
return isWindows() ? Shell.powershell : Shell.bash;
}
}
public static boolean isMac() {
return getOS() == OS.mac;
}
public static String getVendor() {
return System.getenv(JBANG_JDK_VENDOR);
}
/**
* Swizzles the content retrieved from sites that are known to possibly embed
* code (i.e. mastodon and carbon)
*/
public static Path swizzleContent(String fileURL, Path filePath) throws IOException {
boolean mastodon = fileURL.matches("https://.*/@(\\w+)/([0-9]+)");
if (mastodon || fileURL.startsWith("https://carbon.now.sh") || fileURL.startsWith("https://bsky.app/")) { // sites
// known
// to have
// og:description
// meta name or
// property
try {
Document doc = Jsoup.parse(filePath.toFile(), "UTF-8", fileURL);
String proposedString = null;
proposedString = doc.select("meta[property=og:description],meta[name=og:description]")
.first()
.attr("content");
/*
* if (twitter) { // remove fake quotes // proposedString =
* proposedString.replace("\u201c", ""); // proposedString =
* proposedString.replace("\u201d", ""); // unescape properly proposedString =
* Parser.unescapeEntities(proposedString, true);
*
* }
*/
if (proposedString != null) {
Matcher m = mainClassPattern.matcher(proposedString);
Matcher pc = publicClassPattern.matcher(proposedString);
String wantedfilename;
if (m.find()) {
String guessedClass = m.group(1);
wantedfilename = guessedClass + ".java";
} else if (pc.find()) {
String guessedClass = pc.group(1);
wantedfilename = guessedClass + ".java";
} else {
wantedfilename = filePath.getFileName() + ".jsh";
}
File f = filePath.toFile();
File newFile = new File(f.getParent(), wantedfilename);
f.renameTo(newFile);
filePath = newFile.toPath();
writeString(filePath, proposedString);
}
} catch (RuntimeException re) {
// ignore any errors that can be caused by parsing
}
}
// to handle if kubectl-style name (i.e. extension less)
File f = filePath.toFile();
String nonkebabname = f.getName();
if (!f.getName().endsWith(".jar") && !f.getName().endsWith(".jsh")) { // avoid directly downloaded jar files
// getting renamed to .java
nonkebabname = unkebabify(f.getName());
}
if (nonkebabname.equals(f.getName())) {
return filePath;
} else {
File newfile = new File(f.getParent(), nonkebabname);
if (f.renameTo(newfile)) {
return newfile.toPath();
} else {
throw new IllegalStateException("Could not rename downloaded extension-less file to proper .java file");
}
}
}
static private String agent;
static private String getAgentString() {
if (agent == null) {
String version = System.getProperty("java.version") + "/"
+ System.getProperty("java.vm.vendor", "");
agent = "JBang/" + getJBangVersion() + " (" + System.getProperty("os.name") + "/"
+ System.getProperty("os.version") + "/" + System.getProperty("os.arch") + ") " + "Java/" + version;
}
return agent;
}
public static String getJBangVersion() {
return BuildConfig.VERSION;
}
/**
* Either retrieves a previously downloaded file from the cache or downloads a
* file from a URL and stores it in the cache.
*
* @param fileURL HTTP URL of the file to be downloaded
* @return Path to the downloaded file
* @throws IOException
*/
public static Path downloadAndCacheFile(String fileURL) throws IOException {
Path saveDir = getUrlCacheDir(fileURL);
Path metaSaveDir = getCacheMetaDir(saveDir);
Path cachedFile = getCachedFile(saveDir);
if (cachedFile == null || isEvicted(cachedFile)) {
return downloadFileAndCache(fileURL, saveDir, metaSaveDir, cachedFile);
} else {
verboseMsg(String.format("Using cached file %s for remote %s", cachedFile, fileURL));
return saveDir.resolve(cachedFile);
}
}
// Returns true if the cached file doesn't exist or if its last
// modified time is longer ago than the configuration value
// indicated by "cache-evict" (defaults to "0" which will
// cause this method to always return "true").
private static boolean isEvicted(Path cachedFile) {
if (isOffline()) {
return false;
}
if (isFresh()) {
return true;
}
if (Files.isRegularFile(cachedFile)) {
long cma = Settings.getCacheEvict();
if (cma == 0) {
return true;
} else if (cma == -1) {
return false;
}
try {
Instant cachedLastModified = Files.getLastModifiedTime(cachedFile).toInstant();
Duration d = Duration.between(cachedLastModified, Instant.now());
return d.getSeconds() >= cma;
} catch (IOException e) {
return true;
}
} else {
return true;
}
}
private static Path downloadFileAndCache(String fileURL, Path saveDir, Path metaSaveDir, Path cachedFile)
throws IOException {
ConnectionConfigurator cfg = ConnectionConfigurator.all(
ConnectionConfigurator.userAgent(),
ConnectionConfigurator.authentication(),
ConnectionConfigurator.timeout(null),
ConnectionConfigurator.cacheControl(cachedFile, metaSaveDir));
ResultHandler handler = ResultHandler.redirects(cfg,
ResultHandler.handleUnmodified(cachedFile,
ResultHandler.throwOnError(
ResultHandler.downloadToTempDir(saveDir, metaSaveDir,
ResultHandler::downloadTo))));
return connect(fileURL, cfg, handler);
}
/**
* Downloads a file from a URL
*
* @param fileURL HTTP URL of the file to be downloaded
* @param saveDir path of the directory to save the file
* @return Path to the downloaded file
* @throws IOException
*/
public static Path downloadFile(String fileURL, Path saveDir) throws IOException {
return downloadFile(fileURL, saveDir, -1);
}
/**
* Downloads a file from a URL
*
* @param fileURL HTTP URL of the file to be downloaded
* @param saveDir path of the directory to save the file
* @param timeOut the timeout in milliseconds to use for opening the connection.
* 0 is an infinite timeout while -1 uses the defaults
* @return Path to the downloaded file
* @throws IOException
*/
public static Path downloadFile(String fileURL, Path saveDir, Integer timeOut) throws IOException {
ConnectionConfigurator cfg = ConnectionConfigurator.all(
ConnectionConfigurator.userAgent(),
ConnectionConfigurator.authentication(),
ConnectionConfigurator.timeout(timeOut));
ResultHandler handler = ResultHandler.redirects(cfg,
ResultHandler.throwOnError(
ResultHandler.downloadTo(saveDir, saveDir)));
return connect(fileURL, cfg, handler);
}
static Path etagFile(Path cachedFile, Path metaSaveDir) {
return metaSaveDir.resolve(cachedFile.getFileName() + ".etag");
}
private static String safeReadEtagFile(Path cachedFile, Path metaSaveDir) {
Path etag = etagFile(cachedFile, metaSaveDir);
if (Files.isRegularFile(etag)) {
try {
return readString(etag);
} catch (IOException e) {
// Ignore
}
}
return null;
}
private static Path connect(String fileURL, ConnectionConfigurator configurator, ResultHandler resultHandler)
throws IOException {
if (isOffline()) {
throw new FileNotFoundException("jbang is in offline mode, no remote access permitted");
}
URL url = new URL(fileURL);
URLConnection urlConnection = url.openConnection();
configurator.configure(urlConnection);
if (urlConnection instanceof HttpURLConnection) {
HttpURLConnection httpConn = (HttpURLConnection) urlConnection;
verboseMsg(String.format("Requesting HTTP %s %s", httpConn.getRequestMethod(), httpConn.getURL()));
verboseMsg(String.format("Headers %s", httpConn.getRequestProperties()));
} else {
verboseMsg(String.format("Requesting %s", urlConnection.getURL()));
}
try {
return resultHandler.handle(urlConnection);
} finally {
if (urlConnection instanceof HttpURLConnection) {
((HttpURLConnection) urlConnection).disconnect();
}
}
}
private interface ConnectionConfigurator {
void configure(URLConnection conn) throws IOException;
static ConnectionConfigurator all(ConnectionConfigurator... configurators) {
return conn -> {
for (ConnectionConfigurator c : configurators) {
c.configure(conn);
}
};
}
static ConnectionConfigurator userAgent() {
return conn -> {
conn.setRequestProperty("User-Agent", getAgentString());
};
}
static ConnectionConfigurator authentication() {
return Util::addAuthHeaderIfNeeded;
}
interface HttpConnectionConfigurator {
void configure(HttpURLConnection conn) throws IOException;
}
static ConnectionConfigurator forHttp(HttpConnectionConfigurator configurator) {
return conn -> {
if (conn instanceof HttpURLConnection) {
configurator.configure((HttpURLConnection) conn);
}
};
}
static ConnectionConfigurator timeout(Integer timeOut) {
return forHttp(conn -> {
int t = (timeOut != null) ? timeOut : Settings.getConnectionTimeout();
if (t >= 0) {
conn.setConnectTimeout(t);
conn.setReadTimeout(t);
}
});
}
static ConnectionConfigurator cacheControl(Path cachedFile, Path metaSaveDir) throws IOException {
if (cachedFile != null && !isFresh()) {
String cachedETag = safeReadEtagFile(cachedFile, metaSaveDir);
Instant lmt = Files.getLastModifiedTime(cachedFile).toInstant();
ZonedDateTime zlmt = ZonedDateTime.ofInstant(lmt, ZoneId.of("GMT"));
String cachedLastModified = DateTimeFormatter.RFC_1123_DATE_TIME.format(zlmt);
return conn -> {
if (cachedETag != null) {
conn.setRequestProperty("If-None-Match", cachedETag);
}
conn.setRequestProperty("If-Modified-Since", cachedLastModified);
};
} else {
return conn -> {
};
}
}
}
private interface ResultHandler {
Path handle(URLConnection urlConnection) throws IOException;
static ResultHandler redirects(ConnectionConfigurator configurator, ResultHandler okHandler) {
return conn -> {
if (conn instanceof HttpURLConnection) {
conn = handleRedirects((HttpURLConnection) conn, configurator);
}
return okHandler.handle(conn);
};
}
static ResultHandler throwOnError(ResultHandler okHandler) {
return conn -> {
if (conn instanceof HttpURLConnection) {
HttpURLConnection httpConn = (HttpURLConnection) conn;
int responseCode = httpConn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
String fileURL = conn.getURL().toExternalForm();
throw new FileNotFoundException(
"No file to download at " + fileURL + ". Server replied HTTP code: " + responseCode);
} else if (responseCode >= 400) {
String message = null;
if (httpConn.getErrorStream() != null) {
String err = new BufferedReader(new InputStreamReader(httpConn.getErrorStream()))
.lines()
.collect(
Collectors.joining(
"\n"))
.trim();
verboseMsg("HTTP: " + responseCode + " - " + err);
if (err.startsWith("{") && err.endsWith("}")) {
// Could be JSON, let's try to parse it
try {
Gson parser = new Gson();
Map json = parser.fromJson(err, Map.class);
// GitHub returns useful information in `message`,
// if it's there we use it.
// TODO add support for other known sites
message = Objects.toString(json.get("message"));
} catch (JsonSyntaxException ex) {
// Not JSON it seems
}
}
}
if (message != null) {
throw new IOException(
String.format("Server returned HTTP response code: %d for URL: %s with message: %s",
responseCode, conn.getURL().toString(), message));
} else {
throw new IOException(String.format("Server returned HTTP response code: %d for URL: %s",
responseCode, conn.getURL().toString()));
}
}
}
return okHandler.handle(conn);
};
}
static ResultHandler downloadTo(Path saveDir, Path metaSaveDir) {
return (conn) -> {
// copy content from connection to file
String fileName = extractFileName(conn);
Path file = saveDir.resolve(fileName);
Files.createDirectories(saveDir);
Files.createDirectories(metaSaveDir);
try (ReadableByteChannel readableByteChannel = Channels.newChannel(conn.getInputStream());
FileOutputStream fileOutputStream = new FileOutputStream(file.toFile())) {
fileOutputStream.getChannel().transferFrom(readableByteChannel, 0, Long.MAX_VALUE);
}
// create an .etag file if the information is present in the response headers
String etag = conn.getHeaderField("ETag");
if (etag != null) {
writeString(etagFile(file, metaSaveDir), etag);
}
verboseMsg(String.format("Downloaded file %s", conn.getURL().toExternalForm()));
return file;
};
}
static ResultHandler downloadToTempDir(Path saveDir, Path metaSaveDir,
BiFunction downloader) {
return (conn) -> {
// create a temp directory for the downloaded content
Path saveTmpDir = saveDir.getParent().resolve(saveDir.getFileName() + ".tmp");
Path saveOldDir = saveDir.getParent().resolve(saveDir.getFileName() + ".old");
Path metaTmpDir = metaSaveDir.getParent().resolve(metaSaveDir.getFileName() + ".tmp");
Path metaOldDir = metaSaveDir.getParent().resolve(metaSaveDir.getFileName() + ".old");
try {
deletePath(saveTmpDir, true);
deletePath(saveOldDir, true);
deletePath(metaTmpDir, true);
deletePath(metaOldDir, true);
Path saveFilePath = downloader.apply(saveTmpDir, metaTmpDir).handle(conn);
// temporarily save the old content
if (Files.isDirectory(saveDir)) {
Files.move(saveDir, saveOldDir);
}
if (Files.isDirectory(metaSaveDir)) {
Files.move(metaSaveDir, metaOldDir);
}
// rename the folder to its final name
Files.move(saveTmpDir, saveDir);
Files.move(metaTmpDir, metaSaveDir);
// remove any old content
deletePath(saveOldDir, true);
deletePath(metaOldDir, true);
return saveDir.resolve(saveFilePath.getFileName());
} catch (Throwable th) {
// remove the temp folder if anything went wrong
deletePath(saveTmpDir, true);
deletePath(metaTmpDir, true);
// and move the old content back if it exists
if (!Files.isDirectory(saveDir) && Files.isDirectory(saveOldDir)) {
try {
Files.move(saveOldDir, saveDir);
} catch (IOException ex) {
// Ignore
}
}
if (!Files.isDirectory(metaSaveDir) && Files.isDirectory(metaOldDir)) {
try {
Files.move(metaOldDir, metaSaveDir);
} catch (IOException ex) {
// Ignore
}
}
throw th;
}
};
}
static ResultHandler handleUnmodified(Path cachedFile, ResultHandler okHandler) {
if (cachedFile != null) {
return (conn) -> {
if (conn instanceof HttpURLConnection) {
HttpURLConnection httpConn = (HttpURLConnection) conn;
if (httpConn.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
verboseMsg(String.format("Not modified, using cached file %s for remote %s", cachedFile,
conn.getURL().toExternalForm()));
// Update cached file's last modified time
try {
Files.setLastModifiedTime(cachedFile, FileTime.from(ZonedDateTime.now().toInstant()));
} catch (IOException e) {
// There is an issue with Java not being able to set the file's last modified
// time
// on certain systems, resulting in an exception. There's not much we can do
// about
// that, so we'll just ignore it. It does mean that files affected by this will
// be
// re-downloaded every time.
verboseMsg("Unable to set last-modified time for " + cachedFile, e);
}
return cachedFile;
}
}
return okHandler.handle(conn);
};
} else {
return okHandler;
}
}
}
private static String extractFileName(URLConnection urlConnection) throws IOException {
String fileURL = urlConnection.getURL().toExternalForm();
String fileName = "";
if (urlConnection instanceof HttpURLConnection) {
HttpURLConnection httpConn = (HttpURLConnection) urlConnection;
int responseCode = httpConn.getResponseCode();
// always check HTTP response code first
if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
String disposition = httpConn.getHeaderField("Content-Disposition");
if (disposition != null) {
// extracts file name from header field
fileName = getDispositionFilename(disposition);
}
}
if (isBlankString(fileName)) {
// extracts file name from URL if nothing found
int p = fileURL.indexOf("?");
// Strip parameters from the URL (if any)
String simpleUrl = (p > 0) ? fileURL.substring(0, p) : fileURL;
while (simpleUrl.endsWith("/")) {
simpleUrl = simpleUrl.substring(0, simpleUrl.length() - 1);
}
fileName = simpleUrl.substring(simpleUrl.lastIndexOf("/") + 1);
}
} else {
fileName = fileURL.substring(fileURL.lastIndexOf("/") + 1);
}
return fileName;
}
private static HttpURLConnection handleRedirects(HttpURLConnection httpConn, ConnectionConfigurator configurator)
throws IOException {
int responseCode;
int redirects = 0;
while (true) {
httpConn.setInstanceFollowRedirects(false);
responseCode = httpConn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_MULT_CHOICE ||
responseCode == HttpURLConnection.HTTP_MOVED_PERM ||
responseCode == HttpURLConnection.HTTP_MOVED_TEMP ||
responseCode == HttpURLConnection.HTTP_SEE_OTHER ||
responseCode == 307 /* TEMP REDIRECT */ ||
responseCode == 308 /* PERM REDIRECT */) {
if (redirects++ > 8) {
throw new IOException("Too many redirects");
}
String location = httpConn.getHeaderField("Location");
if (location == null) {
throw new IOException("No 'Location' header in redirect");
}
URL url = new URL(httpConn.getURL(), location);
url = new URL(swizzleURL(url.toString()));
verboseMsg("Redirected to: " + url); // Should be debug info
httpConn = (HttpURLConnection) url.openConnection();
if (responseCode == HttpURLConnection.HTTP_SEE_OTHER) {
// This response code forces the method to GET
httpConn.setRequestMethod("GET");
}
configurator.configure(httpConn);
continue;
}
break;
}
return httpConn;
}
private static void addAuthHeaderIfNeeded(URLConnection urlConnection) {
String auth = null;
if (urlConnection.getURL().getHost().endsWith("github.com") && System.getenv().containsKey("GITHUB_TOKEN")) {
auth = "token " + System.getenv("GITHUB_TOKEN");
} else {
String username = System.getenv(JBANG_AUTH_BASIC_USERNAME);
String password = System.getenv(JBANG_AUTH_BASIC_PASSWORD);
if (username != null && password != null) {
String id = username + ":" + password;
String encodedId = Base64.getEncoder().encodeToString(id.getBytes(StandardCharsets.UTF_8));
auth = "Basic " + encodedId;
}
}
if (auth != null) {
urlConnection.setRequestProperty("Authorization", auth);
}
}
public static String getDispositionFilename(String disposition) {
String fileName = "";
int index1 = disposition.toLowerCase().lastIndexOf("filename=");
int index2 = disposition.toLowerCase().lastIndexOf("filename*=");
if (index1 > 0 && index1 > index2) {
fileName = unquote(disposition.substring(index1 + 9));
}
if (index2 > 0 && index2 > index1) {
String encodedName = disposition.substring(index2 + 10);
String[] parts = encodedName.split("'", 3);
if (parts.length == 3) {
String encoding = parts[0].isEmpty() ? "iso-8859-1" : parts[0];
String name = parts[2];
try {
fileName = URLDecoder.decode(name, encoding);
} catch (UnsupportedEncodingException e) {
infoMsg("Content-Disposition contains unsupported encoding " + encoding);
}
}
}
return fileName;
}
public static String unquote(String txt) {
if (txt.startsWith("\"") && txt.endsWith("\"")) {
txt = txt.substring(1, txt.length() - 1);
}
return txt;
}
/**
* Checks if a file was previously downloaded and is available in the cache. The
* result takes into account the connection status and freshness requests.
*
* @param fileURL HTTP URL of the file to check
* @return boolean indicating if the file can be retrieved from the cache
* @throws IOException
*/
public static boolean isFileCached(String fileURL) throws IOException {
Path urlCache = getUrlCacheDir(fileURL);
Path file = getCachedFile(urlCache);
return ((!isFresh() || isOffline()) && file != null);
}
private static Path getCachedFile(Path dir) throws IOException {
if (Files.isDirectory(dir)) {
try (Stream files = Files.list(dir)) {
return files.findFirst().orElse(null);
}
} else {
return null;
}
}
/**
* Returns a path to the file indicated by a path or URL. In case the file is
* referenced by a path no action is performed and the value of updateCache is
* ignored. In case the file is referenced by a URL the file will be downloaded
* from that URL and stored in the cache. NB: In the case of URLs this only work
* when the last part of the URL contains the name of the file to be downloaded!
*
* @param filePathOrURL Path or URL to the file to be retrieved
* @return Path to the downloaded file
* @throws IOException
*/
public static Path obtainFile(String filePathOrURL) throws IOException {
try {
Path file = Paths.get(filePathOrURL);
if (Files.isRegularFile(file)) {
return file;
}
} catch (InvalidPathException ex) {
// Ignore
}
return downloadAndCacheFile(swizzleURL(filePathOrURL));
}
public static String swizzleURL(String url) {
url = url.replaceFirst("^https://github.com/(.*)/blob/(.*)$",
"https://raw.githubusercontent.com/$1/$2");
url = url.replaceFirst("^https://gitlab.com/(.*)/-/blob/(.*)$",
"https://gitlab.com/$1/-/raw/$2");
url = url.replaceFirst("^https://bitbucket.org/(.*)/src/(.*)$",
"https://bitbucket.org/$1/raw/$2");
url = url.replaceFirst("^https://twitter.com/(.*)/status/(.*)$",
"https://mobile.twitter.com/$1/status/$2");
if (isGistURL(url)) {
url = extractFileFromGist(url);
} else {
try {
URI u = new URI(url);
if (u.getPath().endsWith("/")) {
verboseMsg("Directory url, assuming user want to get default application at main.java");
url = url + "main.java";
}
} catch (URISyntaxException e) {
// ignore
}
}
return url;
}
/**
* Takes common patterns and return "parent" urls that are meaningfull to trust.
* i.e. github.com/maxandersen/myproject/myfile.java will offer to trust the
* github.com/maxandersen/myproject project.
*
* @param url
* @return
*/
public static String goodTrustURL(String url) {
String originalUrl = url;
url = url.replaceFirst("^https://gist.github.com/(.*)?/(.*)$",
"https://gist.github.com/$1/");
url = url.replaceFirst("^https://github.com/(.*)/blob/(.*)$",
"https://github.com/$1/");
url = url.replaceFirst("^https://gitlab.com/(.*)/-/blob/(.*)$",
"https://gitlab.com/$1/");
url = url.replaceFirst("^https://bitbucket.org/(.*)/src/(.*)$",
"https://bitbucket.org/$1/");
url = url.replaceFirst("^https://twitter.com/(.*)/status/(.*)$",
"https://twitter.com/$1/");
url = url.replaceFirst("https://repo1.maven.org/maven2/(.*)/[0-9]+.*$",
"https://repo1.maven.org/maven2/$1/");
if (url.equals(originalUrl)) {
java.net.URI uri = null;
try {
uri = new java.net.URI(url);
} catch (URISyntaxException e) {
return url;
}
if (uri.getPath().isEmpty() || uri.getPath().equals("/")) {
return uri.toString();
} else {
URI suggested = (uri.getPath().endsWith("/") ? uri.resolve("..") : uri.resolve("."));
if (suggested.getPath().isEmpty() || suggested.getPath().equals("/")) {
// not returning top domain by default
return originalUrl;
} else {
return suggested.toString();
}
}
}
return url;
}
public static String[] extractOrgProject(String url) {
String orggprj = url.replaceFirst("^https://github.com/(.*)/blob/(.*)$", "$1/$2");
orggprj = orggprj.replaceFirst("^https://raw.githubusercontent.com/(.*)/(.*)$", "$1/$2");
orggprj = orggprj.replaceFirst("^https://gitlab.com/(.*)/-/(blob|raw)/(.*)$", "$1/$2");
orggprj = orggprj.replaceFirst("^https://bitbucket.org/(.*)/(src|raw)/(.*)$", "$1/$2");
if (!orggprj.equals(url)) {
return orggprj.split("/", 2);
} else {
return null;
}
}
public static String getStableID(File backingFile) {
try {
return getStableID(readString(backingFile.toPath()));
} catch (IOException e) {
throw new ExitException(BaseCommand.EXIT_GENERIC_ERROR, e);
}
}
public static String getStableID(String input) {
return getStableID(Stream.of(input));
}
public static String getStableID(Stream inputs) {
final MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new ExitException(-1, e);
}
inputs.forEach(input -> {
digest.update(input.getBytes(StandardCharsets.UTF_8));
});
final byte[] hashbytes = digest.digest();
StringBuilder sb = new StringBuilder();
for (byte b : hashbytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
private static String extractFileFromGist(String url) {
String rawURL = "";
String[] pathPlusAnchor = url.split("#");
String fileName = getFileNameFromGistURL(url);
String gistapi = pathPlusAnchor[0].replaceFirst(
"^https://gist.github.com/(([a-zA-Z0-9\\-]*)/)?(?[a-zA-Z0-9]*)$",
"https://api.github.com/gists/${gistid}");
verboseMsg("Gist url api: " + gistapi);
Gist gist = null;
try {
gist = readJsonFromURL(gistapi, Gist.class);
} catch (IOException e) {
verboseMsg("Error when extracting file from gist url.");
throw new IllegalStateException(e);
}
for (Entry> entry : gist.files.entrySet()) {
String key = entry.getKey();
String lowerCaseKey = key.toLowerCase();
if (key.endsWith(".java") || key.endsWith(".jsh")) {
String[] tmp = entry.getValue().get("raw_url").split("/raw/");
String prefix = tmp[0] + "/raw/";
String suffix = tmp[1].split("/")[1];
String mostRecentVersionRawUrl = prefix + gist.history[0].version + "/" + suffix;
if (!fileName.isEmpty()) { // User wants to run specific Gist file
if ((fileName + ".java").equals(lowerCaseKey) || (fileName + ".jsh").equals(lowerCaseKey))
return mostRecentVersionRawUrl;
} else {
if (key.endsWith(".jsh") || hasMainMethod(entry.getValue().get("content")))
return mostRecentVersionRawUrl;
rawURL = mostRecentVersionRawUrl;
}
}
}
if (!fileName.isEmpty())
throw new IllegalArgumentException("Could not find file: " + fileName);
if (rawURL.isEmpty())
throw new IllegalArgumentException("Gist does not contain any .java or .jsh file.");
return rawURL;
}
private static String getFileNameFromGistURL(String url) {
StringBuilder fileName = new StringBuilder();
String[] pathPlusAnchor = url.split("#");
if (pathPlusAnchor.length == 2) {
String[] anchor = pathPlusAnchor[1].split("-");
if (anchor.length < 2)
throw new IllegalArgumentException("Invalid Gist url: " + url);
fileName = new StringBuilder(anchor[1]);
for (int i = 2; i < anchor.length - 1; ++i)
fileName.append("-").append(anchor[i]);
}
return fileName.toString();
}
public static T readJsonFromURL(String requestURL, Class type) throws IOException {
Path jsonFile = downloadAndCacheFile(requestURL);
try (BufferedReader rdr = Files.newBufferedReader(jsonFile, StandardCharsets.UTF_8)) {
Gson parser = new Gson();
return parser.fromJson(rdr, type);
}
}
public static String repeat(String s, int times) {
// If Java 11 becomes the default we can change this to String::repeat
return String.join("", Collections.nCopies(times, s));
}
static class Gist {
Map> files;
History[] history;
}
static class History {
String version;
}
/**
* Runs the given command + arguments and returns its output (both stdout and
* stderr) as a string
*
* @param cmd The command to execute
* @return The output of the command or null if anything went wrong
*/
public static String runCommand(String... cmd) {
try {
ProcessBuilder pb = CommandBuffer.of(cmd).asProcessBuilder();
pb.redirectErrorStream(true);
Process p = pb.start();
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String cmdOutput = br.lines().collect(Collectors.joining("\n"));
int exitCode = p.waitFor();
if (exitCode == 0) {
return cmdOutput;
} else {
verboseMsg(String.format("Command failed: #%d - %s", exitCode, cmdOutput));
}
} catch (IOException | InterruptedException ex) {
verboseMsg("Error running: " + String.join(" ", cmd), ex);
}
return null;
}
public static boolean deletePath(Path path, boolean quiet) {
Exception[] err = new Exception[] { null };
try {
if (Files.isDirectory(path)) {
verboseMsg("Deleting folder " + path);
Files .walk(path)
.sorted(Comparator.reverseOrder())
.forEach(f -> {
try {
Files.delete(f);
} catch (IOException e) {
err[0] = e;
}
});
} else if (Files.exists(path)) {
verboseMsg("Deleting file " + path);
Files.delete(path);
} else if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
Util.verboseMsg("Deleting broken link " + path);
Files.delete(path);
}
} catch (IOException e) {
err[0] = e;
}
if (!quiet && err[0] != null) {
throw new ExitException(-1, "Could not delete " + path.toString(), err[0]);
}
return err[0] == null;
}
public static void createLink(Path link, Path target) {
if (!Files.exists(link)) {
// On Windows we use junction for directories because their
// creation doesn't require any special privileges.
if (getOS() == OS.windows && Files.isDirectory(target)) {
if (createJunction(link, target.toAbsolutePath())) {
return;
}
} else {
if (createSymbolicLink(link, target.toAbsolutePath())) {
return;
}
}
throw new ExitException(BaseCommand.EXIT_GENERIC_ERROR, "Failed to create link " + link + " -> " + target);
}
}
private static boolean createSymbolicLink(Path link, Path target) {
try {
Files.createSymbolicLink(link, target);
return true;
} catch (IOException e) {
if (isWindows() && e instanceof AccessDeniedException && e.getMessage().contains("privilege")) {
infoMsg(String.format("Creation of symbolic link failed %s -> %s", link, target));
infoMsg("This is a known issue with trying to create symbolic links on Windows.");
infoMsg("See the information available at the link below for a solution:");
infoMsg("https://www.jbang.dev/documentation/guide/latest/usage.html#usage-on-windows");
}
verboseMsg(e.toString());
}
return false;
}
private static boolean createJunction(Path link, Path target) {
if (!Files.exists(link) && Files.exists(link, LinkOption.NOFOLLOW_LINKS)) {
// We automatically remove broken links
deletePath(link, true);
}
return runCommand("cmd.exe", "/c", "mklink", "/j", link.toString(), target.toString()) != null;
}
public static boolean isLink(Path path) throws IOException {
return !path.toAbsolutePath().equals(path.toRealPath());
}
public static Path getUrlCacheDir(String fileURL) {
String urlHash = getStableID(fileURL);
return Settings.getCacheDir(Cache.CacheClass.urls).resolve(urlHash);
}
public static Path getCacheMetaDir(Path cacheDir) {
return cacheDir.getParent().resolve(cacheDir.getFileName() + "-meta");
}
public static boolean hasMainMethod(String content) {
return patternMainMethod.matcher(content).find();
}
public static Optional getMainClass(String content) {
Matcher pc = publicClassPattern.matcher(content);
if (pc.find()) {
return Optional.ofNullable(pc.group(1));
} else {
return Optional.ofNullable(null);
}
}
public static boolean isGistURL(String scriptURL) {
return scriptURL.startsWith("https://gist.github.com/");
}
public static boolean isURL(String str) {
try {
URI uri = new URI(str);
String s = uri.getScheme();
return s != null && (s.equals("https") || s.equals("http") || s.equals("file"));
} catch (URISyntaxException e) {
return false;
}
}
public static boolean isAbsoluteRef(String ref) {
return isRemoteRef(ref) || Paths.get(ref).isAbsolute();
}
public static boolean isRemoteRef(String ref) {
return ref.startsWith("http:") || ref.startsWith("https:") || DependencyUtil.looksLikeAGav(ref);
}
public static boolean isClassPathRef(String ref) {
return ref.startsWith("classpath:");
}
public static boolean isValidPath(String path) {
try {
Paths.get(path);
return true;
} catch (InvalidPathException e) {
return false;
}
}
/**
*
* @param content
* @return the package as declared in the source file, eg: a.b.c
*/
public static Optional getSourcePackage(String content) {
try (Scanner sc = new Scanner(content)) {
while (sc.hasNextLine()) {
String line = sc.nextLine();
if (!line.trim().startsWith("package "))
continue;
String[] pkgLine = line.split("package ");
if (pkgLine.length == 1)
continue;
String packageName = pkgLine[1];
return Optional.of(packageName.split(";")[0].trim()); // remove ';'
}
}
return Optional.empty();
}
public static boolean isValidModuleIdentifier(String id) {
return patternModuleId.matcher(id).matches();
}
public static boolean isValidClassIdentifier(String id) {
return patternFQCN.matcher(id).matches();
}
/**
* Searches the locations defined by PATH for the given executable
*
* @param cmd The name of the executable to look for
* @return A Path to the executable, if found, null otherwise
*/
public static Path searchPath(String cmd) {
String envPath = System.getenv("PATH");
envPath = envPath != null ? envPath : "";
return searchPath(cmd, envPath);
}
/**
* Searches the locations defined by `paths` for the given executable
*
* @param cmd The name of the executable to look for
* @param paths A string containing the paths to search
* @return A Path to the executable, if found, null otherwise
*/
public static Path searchPath(String cmd, String paths) {
return Arrays .stream(paths.split(File.pathSeparator))
.map(dir -> Paths.get(dir).resolve(cmd))
.flatMap(Util::executables)
.filter(Util::isExecutable)
.findFirst()
.orElse(null);
}
private static Stream executables(Path base) {
if (isWindows()) {
return Stream.of(Paths.get(base.toString() + ".exe"),
Paths.get(base.toString() + ".bat"),
Paths.get(base.toString() + ".cmd"),
Paths.get(base.toString() + ".ps1"));
} else {
return Stream.of(base);
}
}
private static boolean isExecutable(Path file) {
if (Files.isRegularFile(file)) {
if (isWindows()) {
String nm = file.getFileName().toString().toLowerCase();
return nm.endsWith(".exe") || nm.endsWith(".bat") || nm.endsWith(".cmd") || nm.endsWith(".ps1");
} else {
return Files.isExecutable(file);
}
}
return false;
}
/**
* Converts a Path to a String. This is normally a trivial operation, but this
* method handles the special case when running in a Bash shell on Windows,
* where paths have a special format that Java doesn't know about (eg.
* C:\Directory\File.txt becomes /c/Directory/File.txt).
*
* @param path the Path to convert
* @return a String representing the given Path
*/
public static String pathToString(Path path) {
if (isWindows() && getShell() == Shell.bash) {
StringBuilder str = new StringBuilder();
if (path.isAbsolute()) {
if (path.getRoot().toString().endsWith(":\\")) {
// Convert `x:` to `/x/`
str.append("/").append(path.getRoot().toString().charAt(0)).append("/");
} else {
// Convert `\\server\share` to `//server/share`
str.append(path.getRoot().toString().replace("\\", "/"));
}
for (int i = 0; i < path.getNameCount(); i++) {
if (i > 0) {
str.append("/");
}
str.append(path.getName(i).toString());
}
}
return str.toString();
} else {
return path.toString();
}
}
/**
* Converts a Path to a String. This is normally a trivial operation, but this
* method handles the special case when running in a Bash shell on Windows,
* where the default output format would cause problems because the backslashes
* would be interpreted as escape sequences. So we need to escape the
* backslashes.
*
* @param path the Path to convert
* @return a String representing the given Path
*/
public static String pathToOsString(Path path) {
if (isWindows() && getShell() == Shell.bash) {
return path.toString().replace("\\", "\\\\");
} else {
return path.toString();
}
}
/**
* Determines if the current JBang we're running was one installed using `app
* install` or not
*/
public static boolean runningManagedJBang() {
try {
return getJarLocation().toRealPath().startsWith(Settings.getConfigBinDir().toRealPath());
} catch (IOException e) {
return getJarLocation().startsWith(Settings.getConfigBinDir());
}
}
/**
* Determines the path to the JAR of the currently running JBang
*
* @return An actual Path if it was found, or an empty path if it was not
*/
public static Path getJarLocation() {
return getJarLocation(VersionChecker.class);
}
/**
* Determines the path to the JAR that contains the given class
*
* @return An actual Path if it was found, or an empty path if it was not
*/
public static Path getJarLocation(Class> klazz) {
try {
File jarFile = new File(klazz.getProtectionDomain().getCodeSource().getLocation().toURI());
return jarFile.toPath();
} catch (URISyntaxException e) {
// ignore
}
return Paths.get("");
}
public static T findNearestWith(Path dir, String fileName, Function accept) {
T result = findNearestLocalWith(dir, fileName, accept);
if (result == null) {
Path file = Settings.getConfigDir().resolve(fileName);
if (Files.isRegularFile(file) && Files.isReadable(file)) {
result = accept.apply(file);
}
}
return result;
}
private static T findNearestLocalWith(Path dir, String fileName, Function accept) {
if (dir == null) {
dir = getCwd();
}
Path root = Settings.getLocalRootDir();
while (dir != null && !isSameFile(dir, root)) {
Path file = dir.resolve(fileName);
if (Files.isRegularFile(file) && Files.isReadable(file)) {
T result = accept.apply(file);
if (result != null) {
return result;
}
}
file = dir.resolve(Settings.JBANG_DOT_DIR).resolve(fileName);
if (Files.isRegularFile(file) && Files.isReadable(file)) {
T result = accept.apply(file);
if (result != null) {
return result;
}
}
dir = dir.getParent();
}
return null;
}
public static boolean isSameFile(Path f1, Path f2) {
try {
return Files.isSameFile(f1, f2);
} catch (IOException e) {
return f1.toAbsolutePath().equals(f2.toAbsolutePath());
}
}
public static boolean isNullOrEmptyString(String str) {
return str == null || str.isEmpty();
}
public static boolean isNullOrBlankString(String str) {
return str == null || isBlankString(str);
}
public static boolean isBlankString(String str) {
return str.trim().isEmpty();
}
public static int askInput(String message, int timeout, int defaultValue, String... options) {
ConsoleInput con = ConsoleInput.get(1, timeout, TimeUnit.SECONDS);
if (con != null) {
StringBuilder msg = new StringBuilder(message + "\n\n");
for (int i = 0; i < options.length; i++) {
msg.append("(").append(i + 1).append(") ").append(options[i]).append("\n");
}
msg.append("(0) Cancel\n");
infoMsg(msg.toString());
while (true) {
infoMsg("Type in your choice and hit enter. Will automatically select option (" + defaultValue
+ ") after " + timeout + " seconds.");
String input = con.readLine();
if (input == null) {
infoMsg("Timeout reached, selecting option (" + defaultValue + ")");
return defaultValue;
}
if (input.isEmpty()) {
break;
}
try {
int result = Integer.parseInt(input);
if (result >= 0 && result <= options.length) {
return result;
}
} catch (NumberFormatException ef) {
errorMsg("Could not parse answer as a number. Canceling");
}
}
} else if (!GraphicsEnvironment.isHeadless()) {
infoMsg("Please make your selection in the pop-up dialog.");
String defOpt = defaultValue > 0 ? options[defaultValue - 1] : "";
setupApplicationIcon();
Object selected = JOptionPane.showInputDialog(null, message, "Select your choice",
JOptionPane.QUESTION_MESSAGE, getJbangIcon(), options, defOpt);
if (selected == null) {
return 0;
}
for (int i = 0; i < options.length; i++) {
if (options[i] == selected) {
return i + 1;
}
}
} else {
errorMsg("No console and no graphical interface, we can't ask for feedback!");
}
return -1;
}
public static boolean haveConsole() {
return !"true".equalsIgnoreCase(System.getenv(JBANG_STDIN_NOTTY));
}
private static void setupApplicationIcon() {
try {
Class> clazz = Util.class.getClassLoader().loadClass("java.awt.Taskbar");
Method getTaskbarMth = clazz.getMethod("getTaskbar");
Object taskbar = getTaskbarMth.invoke(null);
Method setIconImageMth = clazz.getMethod("setIconImage", Image.class);
setIconImageMth.invoke(taskbar, getJbangIcon().getImage());
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
verboseMsg("Unable to set application icon: Taskbar API not available");
} catch (InvocationTargetException e) {
verboseMsg("Unable to set application icon: " + e.getTargetException());
}
}
public static boolean mkdirs(Path p) {
try {
Files.createDirectories(p);
} catch (IOException e) {
return false;
}
return true;
}
private static ImageIcon getJbangIcon() {
URL url = Util.class.getResource("/jbang_icon_64x64.png");
if (url != null) {
return new ImageIcon(url);
} else {
return null;
}
}
@SafeVarargs
public static List join(Collection... lists) {
List res = new ArrayList<>();
for (Collection c : lists) {
if (c != null && !c.isEmpty()) {
res.addAll(c);
}
}
return res;
}
public static String replaceAll(@Nonnull Pattern pattern, @Nonnull String input,
@Nonnull Function replacer) {
Matcher matcher = pattern.matcher(input);
matcher.reset();
boolean result = matcher.find();
if (result) {
StringBuffer sb = new StringBuffer();
do {
String replacement = replacer.apply(matcher);
matcher.appendReplacement(sb, replacement);
result = matcher.find();
} while (result);
matcher.appendTail(sb);
return sb.toString();
}
return input;
}
public static String substituteRemote(String arg) {
if (arg == null) {
return null;
}
return Util.replaceAll(subUrlPattern, arg, m -> {
String txt = m.group().substring(1);
if (txt.startsWith("%")) {
return Matcher.quoteReplacement(txt);
}
if (txt.startsWith("{") && txt.endsWith("}")) {
txt = txt.substring(1, txt.length() - 1);
}
if (txt.startsWith("http://") || txt.startsWith("https://")) {
try {
return Matcher.quoteReplacement(Util.downloadAndCacheFile(txt).toString());
} catch (IOException e) {
throw new ExitException(BaseCommand.EXIT_INVALID_INPUT, "Error substituting remote file: " + txt,
e);
}
} else if (txt.startsWith("deps:")) {
List deps = Arrays.asList(txt.substring(5).split(","));
DependencyResolver resolver = new DependencyResolver();
resolver.addDependencies(deps);
ModularClassPath mcp = resolver.resolve();
return mcp.getClassPath();
} else {
String type = txt.substring(0, txt.indexOf(":"));
throw new ExitException(BaseCommand.EXIT_INVALID_INPUT, "Unknown substitution type: " + type);
}
});
}
public static Entry entry(K k, V v) {
return new AbstractMap.SimpleEntry(k, v);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy