set minSdk to 26 && fix utils

This commit is contained in:
Tungstend 2022-10-19 21:29:48 +08:00
parent 1e31d41c65
commit 55e313a562
14 changed files with 753 additions and 169 deletions

View File

@ -8,7 +8,7 @@ android {
defaultConfig {
applicationId "com.tungsten.fcl"
minSdk 23
minSdk 26
targetSdk 32
versionCode 1
versionName "1.0"

View File

@ -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'

View File

@ -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:

View File

@ -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 -> {

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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() {

View File

@ -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)));
}
}

View File

@ -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 = "=";

View File

@ -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 {

View File

@ -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);
}

View File

@ -7,7 +7,7 @@ android {
compileSdk 32
defaultConfig {
minSdk 23
minSdk 26
targetSdk 32
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"