set minSdk to 26 && fix utils
This commit is contained in:
parent
1e31d41c65
commit
55e313a562
|
@ -8,7 +8,7 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId "com.tungsten.fcl"
|
||||
minSdk 23
|
||||
minSdk 26
|
||||
targetSdk 32
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
|
|
@ -7,7 +7,7 @@ android {
|
|||
compileSdk 32
|
||||
|
||||
defaultConfig {
|
||||
minSdk 23
|
||||
minSdk 26
|
||||
targetSdk 32
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
@ -27,6 +27,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.nanohttpd:nanohttpd:2.3.1'
|
||||
implementation 'org.apache.commons:commons-lang3:3.12.0'
|
||||
implementation 'org.jenkins-ci:constant-pool-scanner:1.2'
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
|
|
|
@ -91,10 +91,31 @@ public final class Lang {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast {@code obj} to V dynamically.
|
||||
* @param obj the object reference to be cast.
|
||||
* @param clazz the class reference of {@code V}.
|
||||
* @param <V> the type that {@code obj} is being cast to.
|
||||
* @return {@code obj} in the type of {@code V}.
|
||||
*/
|
||||
public static <V> Optional<V> tryCast(Object obj, Class<V> clazz) {
|
||||
if (clazz.isInstance(obj)) {
|
||||
return Optional.of(clazz.cast(obj));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T getOrDefault(List<T> a, int index, T defaultValue) {
|
||||
return index < 0 || index >= a.size() ? defaultValue : a.get(index);
|
||||
}
|
||||
|
||||
public static <T> T merge(T a, T b, BinaryOperator<T> operator) {
|
||||
if (a == null) return b;
|
||||
if (b == null) return a;
|
||||
return operator.apply(a, b);
|
||||
}
|
||||
|
||||
public static <T> List<T> removingDuplicates(List<T> list) {
|
||||
LinkedHashSet<T> set = new LinkedHashSet<>(list.size());
|
||||
set.addAll(list);
|
||||
|
@ -218,6 +239,89 @@ public final class Lang {
|
|||
return null;
|
||||
}
|
||||
|
||||
public static <T> T apply(T t, Consumer<T> consumer) {
|
||||
consumer.accept(t);
|
||||
return t;
|
||||
}
|
||||
|
||||
public static void rethrow(Throwable e) {
|
||||
if (e == null)
|
||||
return;
|
||||
if (e instanceof ExecutionException || e instanceof CompletionException) { // including UncheckedException and UncheckedThrowable
|
||||
rethrow(e.getCause());
|
||||
} else if (e instanceof RuntimeException) {
|
||||
throw (RuntimeException) e;
|
||||
} else {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Runnable wrap(ExceptionalRunnable<?> runnable) {
|
||||
return () -> {
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (Exception e) {
|
||||
rethrow(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static <T> Supplier<T> wrap(ExceptionalSupplier<T, ?> supplier) {
|
||||
return () -> {
|
||||
try {
|
||||
return supplier.get();
|
||||
} catch (Exception e) {
|
||||
rethrow(e);
|
||||
throw new InternalError("Unreachable code");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static <T, R> Function<T, R> wrap(ExceptionalFunction<T, R, ?> fn) {
|
||||
return t -> {
|
||||
try {
|
||||
return fn.apply(t);
|
||||
} catch (Exception e) {
|
||||
rethrow(e);
|
||||
throw new InternalError("Unreachable code");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static <T> Consumer<T> wrapConsumer(ExceptionalConsumer<T, ?> fn) {
|
||||
return t -> {
|
||||
try {
|
||||
fn.accept(t);
|
||||
} catch (Exception e) {
|
||||
rethrow(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static <T, E> BiConsumer<T, E> wrap(ExceptionalBiConsumer<T, E, ?> fn) {
|
||||
return (t, e) -> {
|
||||
try {
|
||||
fn.accept(t, e);
|
||||
} catch (Exception ex) {
|
||||
rethrow(ex);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> Consumer<T> compose(Consumer<T>... consumers) {
|
||||
return t -> {
|
||||
for (Consumer<T> consumer : consumers) {
|
||||
consumer.accept(t);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public static <T> Stream<T> toStream(Optional<T> optional) {
|
||||
return optional.map(Stream::of).orElseGet(Stream::empty);
|
||||
}
|
||||
|
||||
public static <T> Iterable<T> toIterable(Enumeration<T> enumeration) {
|
||||
if (enumeration == null) {
|
||||
throw new NullPointerException();
|
||||
|
@ -237,6 +341,10 @@ public final class Lang {
|
|||
};
|
||||
}
|
||||
|
||||
public static <T> Iterable<T> toIterable(Stream<T> stream) {
|
||||
return stream::iterator;
|
||||
}
|
||||
|
||||
public static <T> Iterable<T> toIterable(Iterator<T> iterator) {
|
||||
return () -> iterator;
|
||||
}
|
||||
|
@ -261,6 +369,13 @@ public final class Lang {
|
|||
return task;
|
||||
}
|
||||
|
||||
public static Throwable resolveException(Throwable e) {
|
||||
if (e instanceof ExecutionException || e instanceof CompletionException)
|
||||
return resolveException(e.getCause());
|
||||
else
|
||||
return e;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a useful function to prevent exceptions being eaten when using CompletableFuture.
|
||||
* You can write:
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.tungsten.fclcore.util.io.FileUtils;
|
|||
import com.tungsten.fclcore.util.io.IOUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.text.MessageFormat;
|
||||
|
@ -37,7 +38,7 @@ public final class Logging {
|
|||
return message;
|
||||
}
|
||||
|
||||
public static void start(String logFolder) {
|
||||
public static void start(File logFolder) {
|
||||
LOG.setLevel(Level.ALL);
|
||||
LOG.setUseParentHandlers(false);
|
||||
LOG.setFilter(record -> {
|
||||
|
|
|
@ -1,21 +1,58 @@
|
|||
package com.tungsten.fclcore.util.io;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.tungsten.fclcore.util.Lang;
|
||||
import com.tungsten.fclcore.util.StringUtils;
|
||||
import com.tungsten.fclcore.util.function.ExceptionalConsumer;
|
||||
|
||||
public final class FileUtils {
|
||||
|
||||
private FileUtils() {
|
||||
}
|
||||
|
||||
public static boolean canCreateDirectory(String path) {
|
||||
try {
|
||||
return canCreateDirectory(Paths.get(path));
|
||||
} catch (InvalidPathException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean canCreateDirectory(Path path) {
|
||||
if (Files.isDirectory(path)) return true;
|
||||
else if (Files.exists(path)) return false;
|
||||
else {
|
||||
Path lastPath = path; // always not exist
|
||||
path = path.getParent();
|
||||
// find existent ancestor
|
||||
while (path != null && !Files.exists(path)) {
|
||||
lastPath = path;
|
||||
path = path.getParent();
|
||||
}
|
||||
if (path == null) return false; // all ancestors are nonexistent
|
||||
if (!Files.isDirectory(path)) return false; // ancestor is file
|
||||
try {
|
||||
Files.createDirectory(lastPath); // check permission
|
||||
Files.delete(lastPath); // safely delete empty directory
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String getNameWithoutExtension(String fileName) {
|
||||
return StringUtils.substringBeforeLast(fileName, '.');
|
||||
}
|
||||
|
@ -24,20 +61,33 @@ public final class FileUtils {
|
|||
return StringUtils.substringBeforeLast(file.getName(), '.');
|
||||
}
|
||||
|
||||
public static String getNameWithoutExtension(Path file) {
|
||||
return StringUtils.substringBeforeLast(getName(file), '.');
|
||||
}
|
||||
|
||||
public static String getExtension(File file) {
|
||||
return StringUtils.substringAfterLast(file.getName(), '.');
|
||||
}
|
||||
|
||||
public static String getExtension(Path file) {
|
||||
return StringUtils.substringAfterLast(getName(file), '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is for normalizing ZipPath since Path.normalize of ZipFileSystem does not work properly.
|
||||
*/
|
||||
public static String normalizePath(String path) {
|
||||
return StringUtils.addPrefix(StringUtils.removeSuffix(path, "/", "\\"), "/");
|
||||
}
|
||||
|
||||
public static String getName(String path) {
|
||||
return StringUtils.removeSuffix(new File(path).getName(), "/", "\\");
|
||||
public static String getName(Path path) {
|
||||
if (path.getFileName() == null) return "";
|
||||
return StringUtils.removeSuffix(path.getFileName().toString(), "/", "\\");
|
||||
}
|
||||
|
||||
public static String readText(String file) throws IOException {
|
||||
return readText(new File(file), UTF_8);
|
||||
public static String getName(Path path, String candidate) {
|
||||
if (path.getFileName() == null) return candidate;
|
||||
else return getName(path);
|
||||
}
|
||||
|
||||
public static String readText(File file) throws IOException {
|
||||
|
@ -45,44 +95,122 @@ public final class FileUtils {
|
|||
}
|
||||
|
||||
public static String readText(File file, Charset charset) throws IOException {
|
||||
FileInputStream inputStream = new FileInputStream(file);
|
||||
byte[] bytes = new byte [inputStream.available()];
|
||||
inputStream.read(bytes);
|
||||
inputStream.close();
|
||||
return new String(bytes, charset);
|
||||
return new String(Files.readAllBytes(file.toPath()), charset);
|
||||
}
|
||||
|
||||
public static void writeText(String file, String text) throws IOException {
|
||||
writeBytes(new File(file), text);
|
||||
public static String readText(Path file) throws IOException {
|
||||
return readText(file, UTF_8);
|
||||
}
|
||||
|
||||
public static String readText(Path file, Charset charset) throws IOException {
|
||||
return new String(Files.readAllBytes(file), charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write plain text to file. Characters are encoded into bytes using UTF-8.
|
||||
* <p>
|
||||
* We don't care about platform difference of line separator. Because readText accept all possibilities of line separator.
|
||||
* It will create the file if it does not exist, or truncate the existing file to empty for rewriting.
|
||||
* All characters in text will be written into the file in binary format. Existing data will be erased.
|
||||
*
|
||||
* @param file the path to the file
|
||||
* @param text the text being written to file
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public static void writeText(File file, String text) throws IOException {
|
||||
writeBytes(file, text);
|
||||
writeText(file, text, UTF_8);
|
||||
}
|
||||
|
||||
public static void writeBytes(File file, String text) throws IOException {
|
||||
String parent = file.getParent();
|
||||
makeDirectory(parent);
|
||||
makeFile(file.getAbsolutePath());
|
||||
FileWriter fileWriter = new FileWriter(file.getAbsolutePath());
|
||||
fileWriter.write(text);
|
||||
fileWriter.close();
|
||||
/**
|
||||
* Write plain text to file. Characters are encoded into bytes using UTF-8.
|
||||
* <p>
|
||||
* We don't care about platform difference of line separator. Because readText accept all possibilities of line separator.
|
||||
* It will create the file if it does not exist, or truncate the existing file to empty for rewriting.
|
||||
* All characters in text will be written into the file in binary format. Existing data will be erased.
|
||||
*
|
||||
* @param file the path to the file
|
||||
* @param text the text being written to file
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public static void writeText(Path file, String text) throws IOException {
|
||||
writeText(file, text, UTF_8);
|
||||
}
|
||||
|
||||
public static void deleteDirectory(String directory) throws IOException {
|
||||
if (!new File(directory).exists())
|
||||
/**
|
||||
* Write plain text to file.
|
||||
* <p>
|
||||
* We don't care about platform difference of line separator. Because readText accept all possibilities of line separator.
|
||||
* It will create the file if it does not exist, or truncate the existing file to empty for rewriting.
|
||||
* All characters in text will be written into the file in binary format. Existing data will be erased.
|
||||
*
|
||||
* @param file the path to the file
|
||||
* @param text the text being written to file
|
||||
* @param charset the charset to use for encoding
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public static void writeText(File file, String text, Charset charset) throws IOException {
|
||||
writeBytes(file, text.getBytes(charset));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write plain text to file.
|
||||
* <p>
|
||||
* We don't care about platform difference of line separator. Because readText accept all possibilities of line separator.
|
||||
* It will create the file if it does not exist, or truncate the existing file to empty for rewriting.
|
||||
* All characters in text will be written into the file in binary format. Existing data will be erased.
|
||||
*
|
||||
* @param file the path to the file
|
||||
* @param text the text being written to file
|
||||
* @param charset the charset to use for encoding
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public static void writeText(Path file, String text, Charset charset) throws IOException {
|
||||
writeBytes(file, text.getBytes(charset));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write byte array to file.
|
||||
* It will create the file if it does not exist, or truncate the existing file to empty for rewriting.
|
||||
* All bytes in byte array will be written into the file in binary format. Existing data will be erased.
|
||||
*
|
||||
* @param file the path to the file
|
||||
* @param data the data being written to file
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public static void writeBytes(File file, byte[] data) throws IOException {
|
||||
writeBytes(file.toPath(), data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write byte array to file.
|
||||
* It will create the file if it does not exist, or truncate the existing file to empty for rewriting.
|
||||
* All bytes in byte array will be written into the file in binary format. Existing data will be erased.
|
||||
*
|
||||
* @param file the path to the file
|
||||
* @param data the data being written to file
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public static void writeBytes(Path file, byte[] data) throws IOException {
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.write(file, data);
|
||||
}
|
||||
|
||||
public static void deleteDirectory(File directory)
|
||||
throws IOException {
|
||||
if (!directory.exists())
|
||||
return;
|
||||
|
||||
cleanDirectory(directory);
|
||||
if (!isSymlink(directory))
|
||||
cleanDirectory(directory);
|
||||
|
||||
if (!new File(directory).delete()) {
|
||||
if (!directory.delete()) {
|
||||
String message = "Unable to delete directory " + directory + ".";
|
||||
|
||||
throw new IOException(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean deleteDirectoryQuietly(String directory) {
|
||||
public static boolean deleteDirectoryQuietly(File directory) {
|
||||
try {
|
||||
deleteDirectory(directory);
|
||||
return true;
|
||||
|
@ -91,64 +219,65 @@ public final class FileUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static boolean copyDirectory(String src, String dest) throws IOException {
|
||||
File srcDir = new File(src);
|
||||
File destDir = new File(dest);
|
||||
if (!srcDir.isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
if (!destDir.isDirectory() && !destDir.mkdirs()) {
|
||||
return false;
|
||||
}
|
||||
File[] files = srcDir.listFiles();
|
||||
if (files == null) {
|
||||
return true;
|
||||
}
|
||||
for (File file : files) {
|
||||
File destFile = new File(destDir, file.getName());
|
||||
if (file.isFile()) {
|
||||
if (!copyFile(file.getAbsolutePath(), destFile.getAbsolutePath())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (file.isDirectory()) {
|
||||
if (!copyDirectory(file.getAbsolutePath(), destFile.getAbsolutePath())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
/**
|
||||
* Copy directory.
|
||||
* Paths of all files relative to source directory will be the same as the ones relative to destination directory.
|
||||
*
|
||||
* @param src the source directory.
|
||||
* @param dest the destination directory, which will be created if not existing.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
public static void copyDirectory(Path src, Path dest) throws IOException {
|
||||
copyDirectory(src, dest, path -> true);
|
||||
}
|
||||
|
||||
public static boolean copyDirectoryQuietly(String src, String dest) {
|
||||
try {
|
||||
copyDirectory(src, dest);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
public static void copyDirectory(Path src, Path dest, Predicate<String> filePredicate) throws IOException {
|
||||
Files.walkFileTree(src, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
if (!filePredicate.test(src.relativize(file).toString())) {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
|
||||
Path destFile = dest.resolve(src.relativize(file).toString());
|
||||
Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
if (!filePredicate.test(src.relativize(dir).toString())) {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
|
||||
Path destDir = dest.resolve(src.relativize(dir).toString());
|
||||
Files.createDirectories(destDir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void cleanDirectory(String directory) throws IOException {
|
||||
if (!new File(directory).exists()) {
|
||||
public static void cleanDirectory(File directory)
|
||||
throws IOException {
|
||||
if (!directory.exists()) {
|
||||
if (!makeDirectory(directory))
|
||||
throw new IOException("Failed to create directory: " + directory);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!new File(directory).isDirectory()) {
|
||||
if (!directory.isDirectory()) {
|
||||
String message = directory + " is not a directory";
|
||||
throw new IllegalArgumentException(message);
|
||||
}
|
||||
|
||||
File[] files = new File(directory).listFiles();
|
||||
File[] files = directory.listFiles();
|
||||
if (files == null)
|
||||
throw new IOException("Failed to list contents of " + directory);
|
||||
|
||||
IOException exception = null;
|
||||
for (File file : files)
|
||||
try {
|
||||
forceDelete(file.getAbsolutePath());
|
||||
forceDelete(file);
|
||||
} catch (IOException ioe) {
|
||||
exception = ioe;
|
||||
}
|
||||
|
@ -157,7 +286,7 @@ public final class FileUtils {
|
|||
throw exception;
|
||||
}
|
||||
|
||||
public static boolean cleanDirectoryQuietly(String directory) {
|
||||
public static boolean cleanDirectoryQuietly(File directory) {
|
||||
try {
|
||||
cleanDirectory(directory);
|
||||
return true;
|
||||
|
@ -166,12 +295,13 @@ public final class FileUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static void forceDelete(String file) throws IOException {
|
||||
if (new File(file).isDirectory()) {
|
||||
public static void forceDelete(File file)
|
||||
throws IOException {
|
||||
if (file.isDirectory()) {
|
||||
deleteDirectory(file);
|
||||
} else {
|
||||
boolean filePresent = new File(file).exists();
|
||||
if (!new File(file).delete()) {
|
||||
boolean filePresent = file.exists();
|
||||
if (!file.delete()) {
|
||||
if (!filePresent)
|
||||
throw new FileNotFoundException("File does not exist: " + file);
|
||||
throw new IOException("Unable to delete file: " + file);
|
||||
|
@ -179,62 +309,139 @@ public final class FileUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static boolean copyFile(String srcFile, String destFile) throws IOException {
|
||||
public static boolean isSymlink(File file)
|
||||
throws IOException {
|
||||
Objects.requireNonNull(file, "File must not be null");
|
||||
if (File.separatorChar == '\\')
|
||||
return false;
|
||||
File fileInCanonicalDir;
|
||||
if (file.getParent() == null)
|
||||
fileInCanonicalDir = file;
|
||||
else {
|
||||
File canonicalDir = file.getParentFile().getCanonicalFile();
|
||||
fileInCanonicalDir = new File(canonicalDir, file.getName());
|
||||
}
|
||||
|
||||
return !fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile());
|
||||
}
|
||||
|
||||
public static void copyFile(File srcFile, File destFile)
|
||||
throws IOException {
|
||||
Objects.requireNonNull(srcFile, "Source must not be null");
|
||||
Objects.requireNonNull(destFile, "Destination must not be null");
|
||||
File src = new File(srcFile);
|
||||
File dest = new File(destFile);
|
||||
InputStream inputStream = new BufferedInputStream(new FileInputStream(src));
|
||||
OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(dest));
|
||||
byte[] flush = new byte[1024];
|
||||
int len;
|
||||
while ((len = inputStream.read(flush)) != -1) {
|
||||
outputStream.write(flush,0,len);
|
||||
}
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
return true;
|
||||
if (!srcFile.exists())
|
||||
throw new FileNotFoundException("Source '" + srcFile + "' does not exist");
|
||||
if (srcFile.isDirectory())
|
||||
throw new IOException("Source '" + srcFile + "' exists but is a directory");
|
||||
if (srcFile.getCanonicalPath().equals(destFile.getCanonicalPath()))
|
||||
throw new IOException("Source '" + srcFile + "' and destination '" + destFile + "' are the same");
|
||||
File parentFile = destFile.getParentFile();
|
||||
if (parentFile != null && !FileUtils.makeDirectory(parentFile))
|
||||
throw new IOException("Destination '" + parentFile + "' directory cannot be created");
|
||||
if (destFile.exists() && !destFile.canWrite())
|
||||
throw new IOException("Destination '" + destFile + "' exists but is read-only");
|
||||
|
||||
Files.copy(srcFile.toPath(), destFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
public static boolean copyFileQuietly(String srcFile, String destFile) {
|
||||
try {
|
||||
copyFile(srcFile, destFile);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
public static void copyFile(Path srcFile, Path destFile)
|
||||
throws IOException {
|
||||
Objects.requireNonNull(srcFile, "Source must not be null");
|
||||
Objects.requireNonNull(destFile, "Destination must not be null");
|
||||
if (!Files.exists(srcFile))
|
||||
throw new FileNotFoundException("Source '" + srcFile + "' does not exist");
|
||||
if (Files.isDirectory(srcFile))
|
||||
throw new IOException("Source '" + srcFile + "' exists but is a directory");
|
||||
Path parentFile = destFile.getParent();
|
||||
Files.createDirectories(parentFile);
|
||||
if (Files.exists(destFile) && !Files.isWritable(destFile))
|
||||
throw new IOException("Destination '" + destFile + "' exists but is read-only");
|
||||
|
||||
Files.copy(srcFile, destFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
public static boolean moveFile(String srcFile, String destFile) throws IOException {
|
||||
boolean copy = copyFile(srcFile, destFile);
|
||||
boolean delete = new File(srcFile).delete();
|
||||
return copy && delete;
|
||||
public static void moveFile(File srcFile, File destFile) throws IOException {
|
||||
copyFile(srcFile, destFile);
|
||||
srcFile.delete();
|
||||
}
|
||||
|
||||
public static boolean makeDirectory(String directory) {
|
||||
new File(directory).mkdirs();
|
||||
return new File(directory).isDirectory();
|
||||
public static boolean makeDirectory(File directory) {
|
||||
directory.mkdirs();
|
||||
return directory.isDirectory();
|
||||
}
|
||||
|
||||
public static boolean makeFile(String file) {
|
||||
return makeDirectory(new File(file).getParent()) && (new File(file).exists() || Lang.test(new File(file)::createNewFile));
|
||||
public static boolean makeFile(File file) {
|
||||
return makeDirectory(file.getAbsoluteFile().getParentFile()) && (file.exists() || Lang.test(file::createNewFile));
|
||||
}
|
||||
|
||||
public static boolean rename(String path, String newName) {
|
||||
File file = new File(path);
|
||||
String newPath = path.substring(0, path.lastIndexOf("/") + 1) + newName;
|
||||
File newFile = new File(newPath);
|
||||
return file.renameTo(newFile);
|
||||
}
|
||||
|
||||
public static List<File> listFilesByExtension(String file, String extension) {
|
||||
public static List<File> listFilesByExtension(File file, String extension) {
|
||||
List<File> result = new ArrayList<>();
|
||||
File[] files = new File(file).listFiles();
|
||||
File[] files = file.listFiles();
|
||||
if (files != null)
|
||||
for (File it : files)
|
||||
if (extension.equals(getExtension(it)))
|
||||
result.add(it);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the file is convertible to [java.nio.file.Path] or not.
|
||||
*
|
||||
* @param file the file to be tested
|
||||
* @return true if the file is convertible to Path.
|
||||
*/
|
||||
public static boolean isValidPath(File file) {
|
||||
try {
|
||||
file.toPath();
|
||||
return true;
|
||||
} catch (InvalidPathException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static Optional<Path> tryGetPath(String first, String... more) {
|
||||
if (first == null) return Optional.empty();
|
||||
try {
|
||||
return Optional.of(Paths.get(first, more));
|
||||
} catch (InvalidPathException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public static Path tmpSaveFile(Path file) {
|
||||
return file.toAbsolutePath().resolveSibling("." + file.getFileName().toString() + ".tmp");
|
||||
}
|
||||
|
||||
public static void saveSafely(Path file, String content) throws IOException {
|
||||
Path tmpFile = tmpSaveFile(file);
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(tmpFile, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)) {
|
||||
writer.write(content);
|
||||
}
|
||||
|
||||
try {
|
||||
if (Files.exists(file) && Files.getAttribute(file, "dos:hidden") == Boolean.TRUE) {
|
||||
Files.setAttribute(tmpFile, "dos:hidden", true);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
Files.move(tmpFile, file, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
public static void saveSafely(Path file, ExceptionalConsumer<? super OutputStream, IOException> action) throws IOException {
|
||||
Path tmpFile = tmpSaveFile(file);
|
||||
|
||||
try (OutputStream os = Files.newOutputStream(tmpFile, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)) {
|
||||
action.accept(os);
|
||||
}
|
||||
|
||||
try {
|
||||
if (Files.exists(file) && Files.getAttribute(file, "dos:hidden") == Boolean.TRUE) {
|
||||
Files.setAttribute(tmpFile, "dos:hidden", true);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
Files.move(tmpFile, file, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package com.tungsten.fclcore.util.io;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
public class HttpMultipartRequest implements Closeable {
|
||||
private final String boundary = "*****" + System.currentTimeMillis() + "*****";
|
||||
private final HttpURLConnection urlConnection;
|
||||
private final ByteArrayOutputStream stream;
|
||||
private final String endl = "\r\n";
|
||||
|
||||
public HttpMultipartRequest(HttpURLConnection urlConnection) throws IOException {
|
||||
this.urlConnection = urlConnection;
|
||||
urlConnection.setDoOutput(true);
|
||||
urlConnection.setUseCaches(false);
|
||||
urlConnection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
|
||||
|
||||
stream = new ByteArrayOutputStream();
|
||||
}
|
||||
|
||||
private void addLine(String content) throws IOException {
|
||||
stream.write(content.getBytes(UTF_8));
|
||||
stream.write(endl.getBytes(UTF_8));
|
||||
}
|
||||
|
||||
public HttpMultipartRequest file(String name, String filename, String contentType, InputStream inputStream) throws IOException {
|
||||
addLine("--" + boundary);
|
||||
addLine(String.format("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"", name, filename));
|
||||
addLine("Content-Type: " + contentType);
|
||||
addLine("");
|
||||
IOUtils.copyTo(inputStream, stream);
|
||||
addLine("");
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpMultipartRequest param(String name, String value) throws IOException {
|
||||
addLine("--" + boundary);
|
||||
addLine(String.format("Content-Disposition: form-data; name=\"%s\"", name));
|
||||
addLine("");
|
||||
addLine(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
addLine("--" + boundary + "--");
|
||||
urlConnection.setRequestProperty("Content-Length", "" + stream.size());
|
||||
try (OutputStream os = urlConnection.getOutputStream()) {
|
||||
IOUtils.write(stream.toByteArray(), os);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.tungsten.fclcore.util.io;
|
||||
|
||||
import static com.tungsten.fclcore.util.Lang.mapOf;
|
||||
import static com.tungsten.fclcore.util.Lang.wrap;
|
||||
import static com.tungsten.fclcore.util.gson.JsonUtils.GSON;
|
||||
import static com.tungsten.fclcore.util.io.NetworkUtils.createHttpConnection;
|
||||
import static com.tungsten.fclcore.util.io.NetworkUtils.resolveConnection;
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
package com.tungsten.fclcore.util.io;
|
||||
|
||||
import static com.tungsten.fclcore.util.Lang.mapOf;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.tungsten.fclcore.util.Logging;
|
||||
import com.tungsten.fclcore.util.function.ExceptionalFunction;
|
||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class HttpServer extends NanoHTTPD {
|
||||
private int traceId = 0;
|
||||
protected final List<Route> routes = new ArrayList<>();
|
||||
|
||||
public HttpServer(int port) {
|
||||
super(port);
|
||||
}
|
||||
|
||||
public HttpServer(String hostname, int port) {
|
||||
super(hostname, port);
|
||||
}
|
||||
|
||||
public String getRootUrl() {
|
||||
return "http://localhost:" + getListeningPort();
|
||||
}
|
||||
|
||||
protected void addRoute(Method method, Pattern path, ExceptionalFunction<Request, Response, ?> server) {
|
||||
routes.add(new DefaultRoute(method, path, server));
|
||||
}
|
||||
|
||||
protected static Response ok(Object response) {
|
||||
Logging.LOG.info(String.format("Response %s", JsonUtils.GSON.toJson(response)));
|
||||
return newFixedLengthResponse(Response.Status.OK, "text/json", JsonUtils.GSON.toJson(response));
|
||||
}
|
||||
|
||||
protected static Response notFound() {
|
||||
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "404 not found");
|
||||
}
|
||||
|
||||
protected static Response noContent() {
|
||||
return newFixedLengthResponse(Response.Status.NO_CONTENT, MIME_HTML, "");
|
||||
}
|
||||
|
||||
protected static Response badRequest() {
|
||||
return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_HTML, "400 bad request");
|
||||
}
|
||||
|
||||
protected static Response internalError() {
|
||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "500 internal error");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response serve(IHTTPSession session) {
|
||||
int currentId = traceId++;
|
||||
Logging.LOG.info(String.format("[%d] %s --> %s", currentId, session.getMethod().name(),
|
||||
session.getUri() + Optional.ofNullable(session.getQueryParameterString()).map(s -> "?" + s).orElse("")));
|
||||
|
||||
Response response = null;
|
||||
for (Route route : routes) {
|
||||
if (route.method != session.getMethod()) continue;
|
||||
|
||||
Matcher pathMatcher = route.pathPattern.matcher(session.getUri());
|
||||
if (!pathMatcher.find()) continue;
|
||||
|
||||
response = route.serve(new Request(pathMatcher, mapOf(NetworkUtils.parseQuery(session.getQueryParameterString())), session));
|
||||
break;
|
||||
}
|
||||
|
||||
if (response == null) response = notFound();
|
||||
Logging.LOG.info(String.format("[%d] %s <--", currentId, response.getStatus()));
|
||||
return response;
|
||||
}
|
||||
|
||||
public static abstract class Route {
|
||||
Method method;
|
||||
Pattern pathPattern;
|
||||
|
||||
public Route(Method method, Pattern pathPattern) {
|
||||
this.method = method;
|
||||
this.pathPattern = pathPattern;
|
||||
}
|
||||
|
||||
public Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public Pattern getPathPattern() {
|
||||
return pathPattern;
|
||||
}
|
||||
|
||||
public abstract Response serve(Request request);
|
||||
}
|
||||
|
||||
public static class DefaultRoute extends Route {
|
||||
private final ExceptionalFunction<Request, Response, ?> server;
|
||||
|
||||
public DefaultRoute(Method method, Pattern pathPattern, ExceptionalFunction<Request, Response, ?> server) {
|
||||
super(method, pathPattern);
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response serve(Request request) {
|
||||
try {
|
||||
return server.apply(request);
|
||||
} catch (JsonParseException e) {
|
||||
return badRequest();
|
||||
} catch (Exception e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Error handling " + request.getSession().getUri(), e);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Request {
|
||||
Matcher pathVariables;
|
||||
Map<String, String> query;
|
||||
NanoHTTPD.IHTTPSession session;
|
||||
|
||||
public Request(Matcher pathVariables, Map<String, String> query, NanoHTTPD.IHTTPSession session) {
|
||||
this.pathVariables = pathVariables;
|
||||
this.query = query;
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public Matcher getPathVariables() {
|
||||
return pathVariables;
|
||||
}
|
||||
|
||||
public Map<String, String> getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public NanoHTTPD.IHTTPSession getSession() {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,9 @@ package com.tungsten.fclcore.util.io;
|
|||
import java.io.*;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* This utility class consists of some util methods operating on InputStream/OutputStream.
|
||||
*/
|
||||
public final class IOUtils {
|
||||
|
||||
private IOUtils() {
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package com.tungsten.fclcore.util.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.FileSystemNotFoundException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.CodeSource;
|
||||
import java.util.Optional;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
public final class JarUtils {
|
||||
private JarUtils() {
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private static final Optional<Path> THIS_JAR =
|
||||
Optional.ofNullable(JarUtils.class.getProtectionDomain().getCodeSource())
|
||||
.map(CodeSource::getLocation)
|
||||
.map(url -> {
|
||||
try {
|
||||
return Paths.get(url.toURI());
|
||||
} catch (FileSystemNotFoundException | IllegalArgumentException | URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Files::isRegularFile);
|
||||
|
||||
public static Optional<Path> thisJar() {
|
||||
return THIS_JAR;
|
||||
}
|
||||
|
||||
public static Optional<Manifest> getManifest(Path jar) {
|
||||
try (JarFile file = new JarFile(jar.toFile())) {
|
||||
return Optional.ofNullable(file.getManifest());
|
||||
} catch (IOException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public static Optional<String> getImplementationVersion(Path jar) {
|
||||
return Optional.of(jar).flatMap(JarUtils::getManifest)
|
||||
.flatMap(manifest -> Optional.ofNullable(manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION)));
|
||||
}
|
||||
}
|
|
@ -1,23 +1,9 @@
|
|||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.tungsten.fclcore.util.io;
|
||||
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import static com.tungsten.fclcore.util.Pair.pair;
|
||||
import static com.tungsten.fclcore.util.StringUtils.removeSurrounding;
|
||||
import static com.tungsten.fclcore.util.StringUtils.substringAfter;
|
||||
import static com.tungsten.fclcore.util.StringUtils.substringAfterLast;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
|
@ -26,13 +12,9 @@ import java.util.*;
|
|||
import java.util.Map.Entry;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
import com.tungsten.fclcore.util.Pair;
|
||||
|
||||
public final class NetworkUtils {
|
||||
public static final String PARAMETER_SEPARATOR = "&";
|
||||
public static final String NAME_VALUE_SEPARATOR = "=";
|
||||
|
|
|
@ -4,9 +4,18 @@ import static com.tungsten.fclcore.util.Logging.LOG;
|
|||
|
||||
import com.tungsten.fclcore.util.StringUtils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class CommandBuilder {
|
||||
private static final Pattern UNSTABLE_OPTION_PATTERN = Pattern.compile("-XX:(?<key>[a-zA-Z0-9]+)=(?<value>.*)");
|
||||
private static final Pattern UNSTABLE_BOOLEAN_OPTION_PATTERN = Pattern.compile("-XX:(?<value>[+\\-])(?<key>[a-zA-Z0-9]+)");
|
||||
|
||||
private final List<Item> raw = new ArrayList<>();
|
||||
|
||||
|
@ -69,24 +78,53 @@ public final class CommandBuilder {
|
|||
return null;
|
||||
}
|
||||
|
||||
public String addUnstableDefault(String opt, boolean value) {
|
||||
for (Item item : raw) {
|
||||
final Matcher matcher = UNSTABLE_BOOLEAN_OPTION_PATTERN.matcher(item.arg);
|
||||
if (matcher.matches()) {
|
||||
if (matcher.group("key").equals(opt)) {
|
||||
return item.arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value) {
|
||||
raw.add(new Item("-XX:+" + opt, true));
|
||||
} else {
|
||||
raw.add(new Item("-XX:-" + opt, true));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String addUnstableDefault(String opt, String value) {
|
||||
for (Item item : raw) {
|
||||
final Matcher matcher = UNSTABLE_OPTION_PATTERN.matcher(item.arg);
|
||||
if (matcher.matches()) {
|
||||
if (matcher.group("key").equals(opt)) {
|
||||
return item.arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
raw.add(new Item("-XX:" + opt + "=" + value, true));
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean removeIf(Predicate<String> pred) {
|
||||
return raw.removeIf(i -> pred.test(i.arg));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (int i = 0; i < raw.size(); i++) {
|
||||
if (i != 0) {
|
||||
stringBuilder.append(" ");
|
||||
}
|
||||
stringBuilder.append(raw.get(i).parse ? parse(raw.get(i).arg) : raw.get(i).arg);
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
return raw.stream().map(i -> i.parse ? parse(i.arg) : i.arg).collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
public List<String> asList() {
|
||||
List<String> list = new ArrayList<>();
|
||||
for (Item item : raw) {
|
||||
list.add(item.arg);
|
||||
}
|
||||
return list;
|
||||
return raw.stream().map(i -> i.arg).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<String> asMutableList() {
|
||||
return raw.stream().map(i -> i.arg).collect(Collectors.toCollection(ArrayList::new));
|
||||
}
|
||||
|
||||
private static class Item {
|
||||
|
|
|
@ -24,14 +24,8 @@ public class VersionNumber implements Comparable<VersionNumber> {
|
|||
}
|
||||
|
||||
public static boolean isIntVersionNumber(String version) {
|
||||
boolean bool = true;
|
||||
for (int i = 0; i < version.length(); i++) {
|
||||
if (version.charAt(i) != '.' && (version.charAt(i) < '0' || version.charAt(i) > '9')) {
|
||||
bool = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bool && !version.contains("..") && StringUtils.isNotBlank(version)) {
|
||||
if (version.chars().noneMatch(ch -> ch != '.' && (ch < '0' || ch > '9'))
|
||||
&& !version.contains("..") && StringUtils.isNotBlank(version)) {
|
||||
String[] arr = version.split("\\.");
|
||||
for (String str : arr)
|
||||
if (str.length() > 9)
|
||||
|
@ -315,14 +309,7 @@ public class VersionNumber implements Comparable<VersionNumber> {
|
|||
}
|
||||
|
||||
private static Item parseItem(String buf) {
|
||||
boolean bool = true;
|
||||
for (int i = 0; i < buf.length(); i++) {
|
||||
if (!Character.isDigit(buf.charAt(i))) {
|
||||
bool = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return bool ? new IntegerItem(buf) : new StringItem(buf);
|
||||
return buf.chars().allMatch(Character::isDigit) ? new IntegerItem(buf) : new StringItem(buf);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -349,9 +336,5 @@ public class VersionNumber implements Comparable<VersionNumber> {
|
|||
return canonical.hashCode();
|
||||
}
|
||||
|
||||
public static final Comparator<String> VERSION_COMPARATOR = (s, t1) -> {
|
||||
VersionNumber v = VersionNumber.asVersion(s);
|
||||
VersionNumber v1 = VersionNumber.asVersion(t1);
|
||||
return v.compareTo(v1);
|
||||
};
|
||||
public static final Comparator<String> VERSION_COMPARATOR = Comparator.comparing(VersionNumber::asVersion);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ android {
|
|||
compileSdk 32
|
||||
|
||||
defaultConfig {
|
||||
minSdk 23
|
||||
minSdk 26
|
||||
targetSdk 32
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
|
Loading…
Reference in New Issue