blob: c41e944a7acf111621d83bd9a7999baaab9ad3e5 [file] [log] [blame]
/*
* Copyright (c) 2010-2020 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.sdk.core.util;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.StringSelection;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import org.eclipse.scout.sdk.core.log.SdkLog;
/**
* <h3>{@link CoreUtils}</h3> Holds core utilities.
*
* @since 5.1.0
*/
public final class CoreUtils {
private static final Pattern PATH_SEGMENT_SPLIT_PATTERN = Pattern.compile("/");
@SuppressWarnings("HardcodedLineSeparator")
private static final Pattern REGEX_COMMENT_REMOVE_1 = Pattern.compile("//.*?\r\n");
@SuppressWarnings("HardcodedLineSeparator")
private static final Pattern REGEX_COMMENT_REMOVE_2 = Pattern.compile("//.*?\n");
private static final Pattern REGEX_COMMENT_REMOVE_3 = Pattern.compile("(?s)/\\*.*?\\*/");
private static final ThreadLocal<String> CURRENT_USER_NAME = ThreadLocal.withInitial(() -> null);
private CoreUtils() {
}
/**
* Returns the user name of the current thread. If the current thread has no user name set, the system property is
* returned.<br>
* Use {@link CoreUtils#setUsernameForThread(String)} to define the user name for the current thread.
*
* @return The user name of the thread or the system if no user name is defined on the thread.
*/
public static String getUsername() {
String name = CURRENT_USER_NAME.get();
if (name != null) {
return name;
}
//noinspection AccessOfSystemProperties
return System.getProperty("user.name");
}
/**
* Sets the user name that should be returned by {@link CoreUtils#getUsername()} for the current thread.
*
* @param newUsernameForCurrentThread
* the new user name
*/
public static void setUsernameForThread(String newUsernameForCurrentThread) {
setThreadLocal(CURRENT_USER_NAME, newUsernameForCurrentThread);
}
/**
* Removes all comments in the given java source.
*
* @param methodBody
* The java source
* @return The source with all comments (single line & multi line) removed.
*/
public static String removeComments(CharSequence methodBody) {
if (methodBody == null) {
return null;
}
if (Strings.isBlank(methodBody)) {
return methodBody.toString();
}
String retVal = REGEX_COMMENT_REMOVE_1.matcher(methodBody).replaceAll("");
retVal = REGEX_COMMENT_REMOVE_2.matcher(retVal).replaceAll("");
retVal = REGEX_COMMENT_REMOVE_3.matcher(retVal).replaceAll("");
return retVal;
}
/**
* Deletes the given file or folder.<br>
* In case the given {@link Path} is a folder the contents of the folder are deleted recursively.<br>
* In case the given {@link Path} does not exist this method does nothing.
*
* @param toDelete
* The file or folder to delete. Must not be {@code null}
* @throws IOException
* if there is an error deleting the directory
*/
public static void deleteDirectory(Path toDelete) throws IOException {
if (!Files.exists(Ensure.notNull(toDelete))) {
return;
}
Files.walkFileTree(toDelete, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return super.visitFile(file, attrs);
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
try {
// try to delete the file anyway, even if its attributes could not be read, since delete-only access is theoretically possible
Files.delete(file);
}
catch (IOException e) {
SdkLog.debug("Unable to delete '{}' after failed visit.", file, e);
}
if (exc instanceof NoSuchFileException) {
return FileVisitResult.SKIP_SUBTREE;
}
throw exc;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return super.postVisitDirectory(dir, exc);
}
});
}
/**
* Moves the given directory to the given target directory. This means after this method call the source directory
* does not exist anymore and the target directory contains a new folder with the name of the source and its content.
*
* @param sourceDir
* Must be an existing directory.
* @param targetDir
* Must be an existing directory.
* @throws IOException
* if there is an error moving the directory
*/
public static void moveDirectory(Path sourceDir, Path targetDir) throws IOException {
Ensure.notNull(sourceDir);
Ensure.notNull(targetDir);
Ensure.isDirectory(sourceDir);
Ensure.isDirectory(targetDir);
Path fileName = Ensure.notNull(sourceDir.getFileName());
Path targetPath = targetDir.resolve(fileName.toString());
Files.createDirectories(targetPath); // ensure target exists
if (Objects.equals(Files.getFileStore(sourceDir), Files.getFileStore(targetPath))) {
Files.move(sourceDir, targetPath, StandardCopyOption.REPLACE_EXISTING);
}
else {
Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.copy(file, targetPath.resolve(sourceDir.relativize(file)));
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Files.createDirectories(targetPath.resolve(sourceDir.relativize(dir)));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
}
/**
* Creates a relative {@link URI} which leads from base to child.<br>
* <br>
* <b>Note:</b>This method is capable to also construct relative {@link URI}s with parent references (/../) unlike
* {@link URI#relativize(URI)}.
*
* @param base
* The base {@link URI} from which point the relative {@link URI} should be created. Must not be
* {@code null}.
* @param child
* The target {@link URI} that should be relatively expressed from the point of the base {@link URI}. Must
* not be {@code null}.
* @return A new relative {@link URI} to get to the child {@link URI} from the base {@link URI}.
*/
public static URI relativizeURI(URI base, URI child) {
if (!Objects.equals(base.getAuthority(), child.getAuthority())
|| !Objects.equals(base.getScheme(), child.getScheme())) {
return child;
}
// Normalize paths to remove . and .. segments
base = base.normalize();
child = child.normalize();
String[] bParts = PATH_SEGMENT_SPLIT_PATTERN.split(base.getRawPath());
String[] cParts = PATH_SEGMENT_SPLIT_PATTERN.split(child.getRawPath());
// Discard trailing segment of base path
if (bParts.length > 0 && !base.getPath().endsWith("/")) {
bParts = Arrays.copyOf(bParts, bParts.length - 1);
}
// Remove common prefix segments
int i = 0;
while (i < bParts.length
&& i < cParts.length
&& bParts[i].equals(cParts[i])) {
i++;
}
// Construct the relative path
StringBuilder sb = new StringBuilder();
for (int j = 0; j < (bParts.length - i); j++) {
sb.append("../");
}
for (int j = i; j < cParts.length; j++) {
if (j != i) {
sb.append('/');
}
sb.append(cParts[j]);
}
return URI.create(sb.toString()).normalize();
}
/**
* Gets the parent of the given {@link URI}. This means it removes the last segment of the path of the given
* {@link URI}.
*
* @param uri
* the parent path (one folder up) this {@link URI} is returned.
* @return A new {@link URI} pointing to the parent of the given {@link URI} or {@code null} if the given {@link URI}
* is {@code null}.
*/
public static URI getParentURI(URI uri) {
if (uri == null) {
return null;
}
if (uri.getPath().endsWith("/")) {
return uri.resolve("..");
}
return uri.resolve(".");
}
/**
* Compares the given double values. Returns {@code true} if the difference between the two double values is bigger
* than the given delta.<br>
* <br>
* Special cases:
* <ul>
* <li>{@code -0} and {@code +0} are considered to be equal even though
* {@code Double.valueOf(0d).equals(Double.valueOf(-0d))} returns {@code false}!</li>
* <li>{@link Double#NaN} and {@link Double#NaN} are considered to be equal even though
* {@code Double.NaN == Double.NaN} returns {@code false}!</li>
* </ul>
*
* @param d1
* The first double value
* @param d2
* The second double value
* @param delta
* The difference between the two to be considered equal.
* @return {@code false} if the difference between the two values is less or equal to the given delta.
*/
public static boolean isDoubleDifferent(double d1, double d2, double delta) {
if (Double.compare(d1, d2) == 0) {
// handles NaN, Double.POSITIVE_INFINITY and Double.NEGATIVE_INFINITY
return false;
}
return !(Math.abs(d1 - d2) <= Math.abs(delta));
}
/**
* Executes the specified {@link Supplier} while the specified {@link ThreadLocal} has the specified context value.
*
* @param threadLocal
* The {@link ThreadLocal} in which the context should be stored. The initial value of the
* {@link ThreadLocal} must be {@code null}!
* @param context
* The context to store. May be {@code null}.
* @param callable
* The {@link Supplier} to execute. Must not be {@code null}.
*/
public static <T, R> R callInContext(ThreadLocal<T> threadLocal, T context, Supplier<R> callable) {
T orig = threadLocal.get();
try {
setThreadLocal(threadLocal, context);
return callable.get();
}
finally {
setThreadLocal(threadLocal, orig);
}
}
private static <T> void setThreadLocal(ThreadLocal<T> tl, T context) {
if (context == null) {
tl.remove();
}
else {
tl.set(context);
}
}
/**
* Gets the file extension of the file path specified. This is the part after the last dot in the path given.
*
* @param filePath
* The file path for which the extension should be returned.
* @return The extension if it exists or an empty {@link String} otherwise (never returns {@code null}). The extension
* is always lowercase and does not contain the dot.
*/
public static String extensionOf(String filePath) {
if (Strings.isBlank(filePath)) {
return "";
}
int lastDot = filePath.lastIndexOf('.');
if (lastDot < 0) {
return "";
}
return filePath.substring(lastDot + 1).toLowerCase(Locale.ENGLISH);
}
/**
* Gets the file extension of the {@link Path} specified. This is the part after the last dot in the last segment of
* the given file.
*
* @param file
* The file for which the extension should be returned.
* @return The extension if it exists or an empty {@link String} otherwise (never returns {@code null}). The extension
* is always lowercase and does not contain the dot.
*/
public static String extensionOf(Path file) {
if (file == null) {
return "";
}
Path lastSegment = file.getFileName();
if (lastSegment == null) {
return "";
}
return extensionOf(lastSegment.toString());
}
/**
* Gets the value of {@link Object#toString()} of the specified object if the method has been overwritten with a
* custom implementation.
*
* @param o
* The {@link Object} for which the {@link String} representation should be returned.
* @return An {@link Optional} holding the value of {@link Object#toString()} if it has been overwritten and could be
* invoked successfully. Otherwise an empty {@link Optional} is returned.
*/
@SuppressWarnings("squid:S1181")
public static Optional<String> toStringIfOverwritten(Object o) {
if (o == null) {
return Optional.empty();
}
try {
String declaringClassFqn = o.getClass().getMethod("toString").getDeclaringClass().getName();
if (Object.class.getName().equals(declaringClassFqn) || "kotlin.jvm.internal.Lambda".equals(declaringClassFqn)) {
return Optional.empty();
}
}
catch (NoSuchMethodException e) {
SdkLog.debug("Cannot get toString method of operation {}", o.getClass(), e);
return Optional.empty();
}
String val = null;
try {
val = o.toString();
}
catch (Throwable t) {
SdkLog.warning("Failed toString() invocation on an object of type [" + o.getClass().getName() + ']', t);
}
return Strings.notBlank(val);
}
/**
* Sets the given {@link String} into the system clipboard
*
* @param text
* The text to set
* @return {@code true} if the text has been successfully set to the system clipboard.
*/
public static boolean setTextToClipboard(String text) {
return setTextToClipboard(text, null);
}
/**
* Sets the given {@link String} into the system clipboard
*
* @param text
* The text to set
* @param ownershipLostCallback
* An optional callback invoked if the owner ship of the clipboard is lost.
* @return {@code true} if the text has been successfully set to the system clipboard.
*/
public static boolean setTextToClipboard(String text, ClipboardOwner ownershipLostCallback) {
if (GraphicsEnvironment.isHeadless()) {
return false;
}
try {
StringSelection stringSelection = new StringSelection(text);
ClipboardOwner owner = ownershipLostCallback == null ? stringSelection : ownershipLostCallback;
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(stringSelection, owner);
return true;
}
catch (Exception e) {
SdkLog.debug("Error setting text to system clipboard.", e);
return false;
}
}
}