From edd5c4d2b1f38afce6d93a965cc980fbdef8c6e1 Mon Sep 17 00:00:00 2001 From: Robinson Date: Wed, 2 Mar 2022 23:44:31 +0100 Subject: [PATCH] OS tools converted to kotlin, updated file, common + web utilities. --- src/dorkbox/jna/JnaClassUtils.java | 6 +- src/dorkbox/jna/JnaHelper.java | 2 +- src/dorkbox/jna/linux/AppIndicator.java | 2 +- src/dorkbox/jna/linux/GtkCheck.java | 2 +- src/dorkbox/jna/linux/GtkLoader.java | 2 +- src/dorkbox/jna/linux/GtkTheme.java | 10 +- src/dorkbox/jna/windows/User32.java | 2 +- src/dorkbox/os/OS.java | 505 ---- src/dorkbox/os/OS.kt | 379 +++ src/dorkbox/os/OSType.java | 136 -- src/dorkbox/os/OSType.kt | 85 + src/dorkbox/os/OSUtil.java | 26 +- src/dorkbox/util/CacheUtil.java | 9 +- src/dorkbox/util/CommonUtils.kt | 91 + src/dorkbox/util/Desktop.java | 4 +- src/dorkbox/util/DomainUtils.kt | 210 ++ src/dorkbox/util/FileUtil.java | 1990 ---------------- src/dorkbox/util/FileUtil.kt | 2058 +++++++++++++++++ src/dorkbox/util/FontUtil.java | 2 +- src/dorkbox/util/IO.java | 2 + src/dorkbox/util/ImageUtil.java | 4 +- src/dorkbox/util/LZMA.kt | 62 +- src/dorkbox/util/LocationResolver.java | 2 +- src/dorkbox/util/NativeLoader.java | 4 +- src/dorkbox/util/ParallelProcessor.java | 4 +- src/dorkbox/util/Sys.java | 2 +- src/dorkbox/util/UrlDecoder.kt | 160 ++ src/dorkbox/util/UrlEncoder.kt | 248 ++ src/dorkbox/util/WebUtil.kt | 638 +++++ .../util/collections/ConcurrentIterator.java | 2 +- .../util/properties/PropertiesProvider.java | 2 +- 31 files changed, 3936 insertions(+), 2715 deletions(-) delete mode 100644 src/dorkbox/os/OS.java create mode 100644 src/dorkbox/os/OS.kt delete mode 100644 src/dorkbox/os/OSType.java create mode 100644 src/dorkbox/os/OSType.kt create mode 100644 src/dorkbox/util/CommonUtils.kt create mode 100644 src/dorkbox/util/DomainUtils.kt delete mode 100644 src/dorkbox/util/FileUtil.java create mode 100644 src/dorkbox/util/FileUtil.kt create mode 100644 src/dorkbox/util/UrlDecoder.kt create mode 100644 src/dorkbox/util/UrlEncoder.kt create mode 100644 src/dorkbox/util/WebUtil.kt diff --git a/src/dorkbox/jna/JnaClassUtils.java b/src/dorkbox/jna/JnaClassUtils.java index 0d4c010..a2b8161 100644 --- a/src/dorkbox/jna/JnaClassUtils.java +++ b/src/dorkbox/jna/JnaClassUtils.java @@ -55,8 +55,8 @@ public class JnaClassUtils { private static final String libName; static { - if (OS.isMacOsX()) { - if (OS.javaVersion < 7) { + if (OS.INSTANCE.isMacOsX()) { + if (OS.INSTANCE.javaVersion < 7) { libName = "JavaVM"; } else { String javaLocation = System.getProperty("java.home"); @@ -77,7 +77,7 @@ public class JnaClassUtils { Map options = new HashMap(); options.put(Library.OPTION_ALLOW_OBJECTS, Boolean.TRUE); - if (OS.isWindows() && OS.is32bit()) { + if (OS.INSTANCE.isWindows() && OS.INSTANCE.is32bit()) { options.put(Library.OPTION_FUNCTION_MAPPER, new StdCallFunctionMapper() { @Override public diff --git a/src/dorkbox/jna/JnaHelper.java b/src/dorkbox/jna/JnaHelper.java index 5c51dd3..592eed6 100644 --- a/src/dorkbox/jna/JnaHelper.java +++ b/src/dorkbox/jna/JnaHelper.java @@ -37,7 +37,7 @@ class JnaHelper { final Map options = new HashMap(); options.put(Library.OPTION_CLASSLOADER, clazz.getClassLoader()); - if (OS.isWindows()) { + if (OS.INSTANCE.isWindows()) { Set> entries = W32APIOptions.DEFAULT_OPTIONS.entrySet(); for (Map.Entry entry : entries) { options.put(entry.getKey(), entry.getValue()); diff --git a/src/dorkbox/jna/linux/AppIndicator.java b/src/dorkbox/jna/linux/AppIndicator.java index 7e4b9d5..6eec9b5 100644 --- a/src/dorkbox/jna/linux/AppIndicator.java +++ b/src/dorkbox/jna/linux/AppIndicator.java @@ -45,7 +45,7 @@ class AppIndicator { static { boolean _isLoaded = false; - boolean shouldLoadAppIndicator = !(OS.isWindows() || OS.isMacOsX()); + boolean shouldLoadAppIndicator = !(OS.INSTANCE.isWindows() || OS.INSTANCE.isMacOsX()); if (!shouldLoadAppIndicator) { _isLoaded = true; } diff --git a/src/dorkbox/jna/linux/GtkCheck.java b/src/dorkbox/jna/linux/GtkCheck.java index 86b33cd..64cf508 100644 --- a/src/dorkbox/jna/linux/GtkCheck.java +++ b/src/dorkbox/jna/linux/GtkCheck.java @@ -118,7 +118,7 @@ class GtkCheck { */ // java 8 cannot load GTK3. But we can know if GTK was loaded yet or not - if (OS.javaVersion <= 8) { + if (OS.INSTANCE.javaVersion <= 8) { try { // Don't want to load the toolkit!!! Class toolkitClass = Class.forName("java.awt.Toolkit"); diff --git a/src/dorkbox/jna/linux/GtkLoader.java b/src/dorkbox/jna/linux/GtkLoader.java index 4d4af34..9094f82 100644 --- a/src/dorkbox/jna/linux/GtkLoader.java +++ b/src/dorkbox/jna/linux/GtkLoader.java @@ -88,7 +88,7 @@ class GtkLoader { int minor = 0; int micro = 0; - boolean shouldLoadGtk = !(OS.isWindows() || OS.isMacOsX()); + boolean shouldLoadGtk = !(OS.INSTANCE.isWindows() || OS.INSTANCE.isMacOsX()); if (!shouldLoadGtk) { _isLoaded = true; } diff --git a/src/dorkbox/jna/linux/GtkTheme.java b/src/dorkbox/jna/linux/GtkTheme.java index e6222c6..98a591f 100644 --- a/src/dorkbox/jna/linux/GtkTheme.java +++ b/src/dorkbox/jna/linux/GtkTheme.java @@ -48,10 +48,10 @@ import dorkbox.util.MathUtil; public class GtkTheme { /** Fallback for an unknown tray image size. */ - public static volatile int TRAY_IMAGE_SIZE_FALLBACK = OS.getInt(GtkTheme.class.getCanonicalName() + ".TRAY_IMAGE_SIZE_FALLBACK", 24); + public static volatile int TRAY_IMAGE_SIZE_FALLBACK = OS.INSTANCE.getInt(GtkTheme.class.getCanonicalName() + ".TRAY_IMAGE_SIZE_FALLBACK", 24); /** Fallback for an unknown tray menu image size. */ - public static volatile int TRAY_MENU_IMAGE_SIZE_FALLBACK = OS.getInt(GtkTheme.class.getCanonicalName() + ".TRAY_MENU_IMAGE_SIZE_FALLBACK", 16); + public static volatile int TRAY_MENU_IMAGE_SIZE_FALLBACK = OS.INSTANCE.getInt(GtkTheme.class.getCanonicalName() + ".TRAY_MENU_IMAGE_SIZE_FALLBACK", 16); public static Rectangle getPixelTextHeight(String text) { @@ -279,7 +279,7 @@ class GtkTheme { try { File customSettings = new File("/usr/bin/startkde-custom"); if (customSettings.canRead()) { - List lines = FileUtil.readLines(customSettings); + List lines = FileUtil.INSTANCE.readLines(customSettings); for (String line : lines) { String str = "export GDK_SCALE="; int i = line.indexOf(str); @@ -327,7 +327,7 @@ class GtkTheme { File mainFile = new File("/usr/share/plasma/plasmoids/org.kde.plasma.private.systemtray/contents/config/main.xml"); if (mainFile.canRead()) { - List lines = FileUtil.readLines(mainFile); + List lines = FileUtil.INSTANCE.readLines(mainFile); boolean found = false; int index; for (final String line : lines) { @@ -394,7 +394,7 @@ class GtkTheme { if (env == OSUtil.DesktopEnv.Env.XFCE) { // xfce is easy, because it's not a GTK setting for the size (xfce notification area maximum icon size) String properties = OSUtil.DesktopEnv.queryXfce("xfce4-panel", null); - String[] propertiesAsList = properties.split(OS.LINE_SEPARATOR); + String[] propertiesAsList = properties.split(OS.INSTANCE.LINE_SEPARATOR); for (String prop : propertiesAsList) { if (prop.startsWith("/plugins/") && prop.endsWith("/size-max")) { // this is the property we are looking for (we just don't know which panel it's on) diff --git a/src/dorkbox/jna/windows/User32.java b/src/dorkbox/jna/windows/User32.java index 759f112..adf62e5 100644 --- a/src/dorkbox/jna/windows/User32.java +++ b/src/dorkbox/jna/windows/User32.java @@ -37,7 +37,7 @@ import dorkbox.os.OS; @SuppressWarnings("WeakerAccess") public interface User32 { - User32 User32 = OS.is64bit() ? new User32_64() : new User32_32(); + User32 User32 = OS.INSTANCE.is64bit() ? new User32_64() : new User32_32(); int GWL_WNDPROC = -4; diff --git a/src/dorkbox/os/OS.java b/src/dorkbox/os/OS.java deleted file mode 100644 index 0dc7d0f..0000000 --- a/src/dorkbox/os/OS.java +++ /dev/null @@ -1,505 +0,0 @@ -/* - * Copyright 2010 dorkbox, llc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dorkbox.os; - -import java.awt.Color; -import java.io.File; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.Locale; -import java.util.TimeZone; - -public -class OS { - public static final String LINE_SEPARATOR = getProperty("line.separator", "\n"); // make the default unix - public static final String LINE_SEPARATOR_UNIX = "\n"; - public static final String LINE_SEPARATOR_WINDOWS = "\r\n"; - - public static final Charset US_ASCII = StandardCharsets.US_ASCII; - public static final Charset UTF_8 = StandardCharsets.UTF_8; - - public static final File TEMP_DIR = new File(getProperty("java.io.tmpdir", "temp")); - - /** - * The currently running MAJOR java version as a NUMBER. For example, "Java version 1.7u45", and converts it into 7, uses JEP 223 for java > 9 - */ - public static final int javaVersion = _getJavaVersion(); - - - private static final OSType osType; - private static final String originalTimeZone = TimeZone.getDefault().getID(); - - static { - if (!TEMP_DIR.isDirectory()) { - // create the temp dir if necessary because the TEMP dir doesn't exist. - TEMP_DIR.mkdirs(); - } - - String osName = getProperty("os.name", ""); - String osArch = getProperty("os.arch", ""); - - if (osName != null && osArch != null) { - osName = osName.toLowerCase(Locale.US); - osArch = osArch.toLowerCase(Locale.US); - - if (osName.startsWith("linux")) { - // best way to determine if it's android. - // Sometimes java binaries include Android classes on the classpath, even if it isn't actually Android, so we check the VM - - - String value = getProperty("java.vm.name", ""); - boolean isAndroid = "Dalvik".equals(value); - if (isAndroid) { - // android check from https://stackoverflow.com/questions/14859954/android-os-arch-output-for-arm-mips-x86 - if (osArch.equals("armeabi")) { - // really old/low-end non-hf 32bit cpu - osType = OSType.AndroidArm56; - } - else if (osArch.equals("armeabi-v7a")) { - // 32bit hf cpu - osType = OSType.AndroidArm7; - } - else if (osArch.equals("arm64-v8a")) { - // 64bit hf cpu - osType = OSType.AndroidArm8; - } - else if (osArch.equals("x86")) { - // 32bit x86 (usually emulator) - osType = OSType.AndroidX86; - } - else if (osArch.equals("x86_64")) { - // 64bit x86 (usually emulator) - osType = OSType.AndroidX86_64; - } - else if (osArch.equals("mips")) { - // 32bit mips - osType = OSType.AndroidMips; - } - else if (osArch.equals("mips64")) { - // 64bit mips - osType = OSType.AndroidMips64; - } else { - // who knows? - osType = null; - } - } - else { - // normal linux 32/64/arm32/arm64 - if ("amd64".equals(osArch)) { - osType = OSType.Linux64; - } - else { - if (osArch.startsWith("arm")) { - if (osArch.contains("v8")) { - osType = OSType.LinuxArm64; - } - else { - osType = OSType.LinuxArm32; - } - } - else { - osType = OSType.Linux32; - } - } - } - } - else if (osName.startsWith("windows")) { - if ("amd64".equals(osArch)) { - osType = OSType.Windows64; - } - else { - osType = OSType.Windows32; - } - } - else if (osName.startsWith("mac") || osName.startsWith("darwin")) { - if ("x86_64".equals(osArch)) { - osType = OSType.MacOsX64; - } - else { - osType = OSType.MacOsX32; - } - } - else if (osName.startsWith("freebsd") || osName.contains("nix") || osName.contains("nux") || osName.startsWith("aix")) { - if ("x86".equals(osArch) || "i386".equals(osArch)) { - osType = OSType.Unix32; - } - else if ("arm".equals(osArch)) { - osType = OSType.UnixArm; - } - else { - osType = OSType.Unix64; - } - } - else if (osName.startsWith("solaris") || osName.startsWith("sunos")) { - osType = OSType.Solaris; - } - else { - osType = null; - } - } - else { - osType = null; - } - - /* - * By default, the timer resolution on Windows ARE NOT high-resolution (16ms vs 1ms) - * - * 'Thread.sleep(1)' will not really sleep for 1ms, but will really sleep for ~16ms. This long-running sleep will trick Windows - * into using higher resolution timers. - * - * See: https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6435126 - */ - if (osType == null || osType.isWindows()) { - // only necessary on windows - Thread timerAccuracyThread = new Thread(new Runnable() { - @SuppressWarnings("BusyWait") - @Override - public - void run() { - while (true) { - try { - Thread.sleep(Long.MAX_VALUE); - } catch (Exception ignored) { - } - } - } - }, "FixWindowsHighResTimer"); - timerAccuracyThread.setDaemon(true); - timerAccuracyThread.start(); - } - } - - /** - * @return the System Property in a safe way for a given property, or null if it does not exist. - */ - public static - String getProperty(final String property) { - return getProperty(property, null); - } - - /** - * @return the System Property in a safe way for a given property, and if null - returns the specified default value. - */ - public static - String getProperty(final String property, final String defaultValue) { - try { - if (System.getSecurityManager() == null) { - return System.getProperty(property, defaultValue); - } else { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return System.getProperty(property, defaultValue); - } - }); - } - } catch (Exception ignored) { - return defaultValue; - } - } - - /** - * @return the value of the Java system property with the specified {@code property}, while falling back to the - * specified default value if the property access fails. - */ - public static boolean getBoolean(String property, boolean defaultValue) { - String value = getProperty(property); - if (value == null) { - return defaultValue; - } - - value = value.trim().toLowerCase(); - if (value.isEmpty()) { - return defaultValue; - } - - if ("false".equals(value) || "no".equals(value) || "0".equals(value)) { - return false; - } - - if ("true".equals(value) || "yes".equals(value) || "1".equals(value)) { - return true; - } - - return defaultValue; - } - - /** - * @return the value of the Java system property with the specified {@code property}, while falling back to the - * specified default value if the property access fails. - */ - public static int getInt(String property, int defaultValue) { - String value = getProperty(property); - if (value == null) { - return defaultValue; - } - - value = value.trim(); - try { - return Integer.parseInt(value); - } catch (Exception ignored) { - } - - return defaultValue; - } - - /** - * @return the value of the Java system property with the specified {@code property}, while falling back to the - * specified default value if the property access fails. - */ - public static long getLong(String property, long defaultValue) { - String value = getProperty(property); - if (value == null) { - return defaultValue; - } - - value = value.trim(); - try { - return Long.parseLong(value); - } catch (Exception ignored) { - } - - return defaultValue; - } - - /** - * @return the value of the Java system property with the specified {@code property}, while falling back to the - * specified default value if the property access fails. - */ - public static float getFloat(String property, float defaultValue) { - String value = getProperty(property); - if (value == null) { - return defaultValue; - } - - value = value.trim(); - try { - return Float.parseFloat(value); - } catch (Exception ignored) { - } - - return defaultValue; - } - - /** - * @return the value of the Java system property with the specified {@code property}, while falling back to the - * specified default value if the property access fails. - */ - public static double getDouble(String property, double defaultValue) { - String value = getProperty(property); - if (value == null) { - return defaultValue; - } - - value = value.trim(); - try { - return Double.parseDouble(value); - } catch (Exception ignored) { - } - - return defaultValue; - } - - public static - Color getColor(final String property, final Color defaultValue) { - String value = getProperty(property); - if (value == null) { - return defaultValue; - } - - value = value.trim(); - try { - return Color.decode(value); - } catch (Exception ignored) { - } - - return defaultValue; - } - - /** - * @return the OS Type that is running on this machine - */ - public static - OSType get() { - return osType; - } - - public static - boolean is64bit() { - return osType.is64bit(); - } - - public static - boolean is32bit() { - return osType.is32bit(); - } - - /** - * @return true if this is a "standard" x86/x64 architecture (intel/amd/etc) processor. - */ - public static - boolean isX86() { - return osType.isX86(); - } - - public static - boolean isMips() { - return osType.isMips(); - } - - public static - boolean isArm() { - return osType.isArm(); - } - - public static - boolean isLinux() { - return osType.isLinux(); - } - - public static - boolean isUnix() { - return osType.isUnix(); - } - - public static - boolean isSolaris() { - return osType.isSolaris(); - } - - public static - boolean isWindows() { - return osType.isWindows(); - } - - public static - boolean isMacOsX() { - return osType.isMacOsX(); - } - - public static - boolean isAndroid() { - return osType.isAndroid(); - } - - - /** - * Gets the currently running MAJOR java version as a NUMBER. For example, "Java version 1.7u45", and converts it into 7, uses JEP 223 for java > 9 - */ - private static - int _getJavaVersion() { - // this should never be a problem, but just in case - String fullJavaVersion = getProperty("java.version", ""); - - - if (fullJavaVersion.startsWith("1.")) { - switch (fullJavaVersion.charAt(2)) { - case '4': - return 4; - case '5': - return 5; - case '6': - return 6; - case '7': - return 7; - case '8': - return 8; - case '9': - return 9; - } - } - else { - // We are >= java 10, use JEP 223 to get the version (early releases of 9 might not have JEP 223, so 10 is guaranteed to have it) - fullJavaVersion = getProperty("java.specification.version", "10"); - - try { - // it will ALWAYS be the major release version as an integer. See http://openjdk.java.net/jeps/223 - return Integer.parseInt(fullJavaVersion); - } catch (Exception ignored) { - } - } - - // the last valid guess we have, since the current Java implementation, whatever it is, decided not to cooperate with JEP 223. - return 10; - } - - /** - * Set our system to UTC time zone. Retrieve the original time zone via {@link #getOriginalTimeZone()} - */ - public static - void setUTC() { - // have to set our default timezone to UTC. EVERYTHING will be UTC, and if we want local, we must explicitly ask for it. - TimeZone.setDefault(TimeZone.getTimeZone("UTC")); - } - - /** - * Returns the *ORIGINAL* system time zone, before (*IF*) it was changed to UTC - */ - public static - String getOriginalTimeZone() { - return originalTimeZone; - } - - /** - * @return the optimum number of threads for a given task. Makes certain not to take ALL the threads, always returns at least one - * thread. - */ - public static - int getOptimumNumberOfThreads() { - return Math.max(Runtime.getRuntime() - .availableProcessors() - 2, 1); - } - - - /** - * @return the first line of the exception message from 'throwable', or the type if there was no message. - */ - public static - String getExceptionMessage(final Throwable throwable) { - String message = throwable.getMessage(); - if (message != null) { - int index = message.indexOf(OS.LINE_SEPARATOR); - if (index > -1) { - message = message.substring(0, index); - } - } else { - message = throwable.getClass() - .getSimpleName(); - } - - return message; - } - - @Override - public final - Object clone() throws CloneNotSupportedException { - throw new CloneNotSupportedException(); - } - - public final - void writeObject(ObjectOutputStream out) throws java.io.IOException { - throw new java.io.NotSerializableException(); - } - - public final - void readObject(ObjectInputStream in) throws java.io.IOException { - throw new java.io.NotSerializableException(); - } -} diff --git a/src/dorkbox/os/OS.kt b/src/dorkbox/os/OS.kt new file mode 100644 index 0000000..5704cbb --- /dev/null +++ b/src/dorkbox/os/OS.kt @@ -0,0 +1,379 @@ +/* + * Copyright 2010 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("unused") + +package dorkbox.os + +import java.awt.Color +import java.io.File +import java.security.AccessController +import java.security.PrivilegedAction +import java.util.* + +object OS { + // make the default unix + @kotlin.jvm.JvmField + val LINE_SEPARATOR = getProperty("line.separator", "\n")!! + + const val LINE_SEPARATOR_UNIX = "\n" + const val LINE_SEPARATOR_WINDOWS = "\r\n" + + @kotlin.jvm.JvmField + val TEMP_DIR = File(getProperty("java.io.tmpdir", "temp")!!).absoluteFile + + /** + * The currently running MAJOR java version as a NUMBER. For example, "Java version 1.7u45", and converts it into 7, uses JEP 223 for java > 9 + */ + @kotlin.jvm.JvmField + val javaVersion = _getJavaVersion() + private var osType: OSType? = null + + /** + * Returns the *ORIGINAL* system time zone, before (*IF*) it was changed to UTC + */ + val originalTimeZone = TimeZone.getDefault().id + + init { + if (!TEMP_DIR.isDirectory) { + // create the temp dir if necessary because the TEMP dir doesn't exist. + TEMP_DIR.mkdirs() + } + var osName = getProperty("os.name", "")!! + var osArch = getProperty("os.arch", "")!! + + osName = osName.lowercase() + osArch = osArch.lowercase() + if (osName.startsWith("linux")) { + // best way to determine if it's android. + // Sometimes java binaries include Android classes on the classpath, even if it isn't actually Android, so we check the VM + val value = getProperty("java.vm.name", "") + val isAndroid = "Dalvik" == value + if (isAndroid) { + // android check from https://stackoverflow.com/questions/14859954/android-os-arch-output-for-arm-mips-x86 + when (osArch) { + "armeabi" -> { + // really old/low-end non-hf 32bit cpu + osType = OSType.AndroidArm56 + } + "armeabi-v7a" -> { + // 32bit hf cpu + osType = OSType.AndroidArm7 + } + "arm64-v8a" -> { + // 64bit hf cpu + osType = OSType.AndroidArm8 + } + "x86" -> { + // 32bit x86 (usually emulator) + osType = OSType.AndroidX86 + } + "x86_64" -> { + // 64bit x86 (usually emulator) + osType = OSType.AndroidX86_64 + } + "mips" -> { + // 32bit mips + osType = OSType.AndroidMips + } + "mips64" -> { + // 64bit mips + osType = OSType.AndroidMips64 + } + else -> { + // who knows? + osType = null + } + } + } else { + when { + osArch == "amd64" -> { + // normal linux 32/64/arm32/arm64 + osType = OSType.Linux64 + } + osArch.startsWith("arm") -> { + if (osArch.contains("v8")) { + OSType.LinuxArm64 + } else { + OSType.LinuxArm32 + } + } + else -> { + OSType.Linux32 + } + } + } + } else if (osName.startsWith("windows")) { + osType = if ("amd64" == osArch) { + OSType.Windows64 + } else { + OSType.Windows32 + } + } else if (osName.startsWith("mac") || osName.startsWith("darwin")) { + osType = if ("x86_64" == osArch) { + OSType.MacOsX64 + } else { + OSType.MacOsX32 + } + } else if (osName.startsWith("freebsd") || osName.contains("nix") || osName.contains("nux") || osName.startsWith( + "aix" + ) + ) { + osType = when (osArch) { + "x86", "i386" -> { + OSType.Unix32 + } + "arm" -> { + OSType.UnixArm + } + else -> { + OSType.Unix64 + } + } + } else if (osName.startsWith("solaris") || osName.startsWith("sunos")) { + osType = OSType.Solaris + } else { + osType = null + } + + /* + * By default, the timer resolution on Windows ARE NOT high-resolution (16ms vs 1ms) + * + * 'Thread.sleep(1)' will not really sleep for 1ms, but will really sleep for ~16ms. This long-running sleep will trick Windows + * into using higher resolution timers. + * + * See: https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6435126 + */ + val osType_ = osType + if (osType_ == null || osType_.isWindows) { + // only necessary on windows + val timerAccuracyThread = Thread( + { + while (true) { + try { + Thread.sleep(Long.MAX_VALUE) + } catch (ignored: Exception) { + } + } + }, "FixWindowsHighResTimer") + timerAccuracyThread.isDaemon = true + timerAccuracyThread.start() + } + } + + /** + * @return the System Property in a safe way for a given property, or null if it does not exist. + */ + fun getProperty(property: String): String? { + return getProperty(property, null) + } + + /** + * @return the System Property in a safe way for a given property, and if null - returns the specified default value. + */ + fun getProperty(property: String, defaultValue: String?): String? { + return try { + if (System.getSecurityManager() == null) { + System.getProperty(property, defaultValue) + } else { + AccessController.doPrivileged(PrivilegedAction { System.getProperty(property, defaultValue) }) + } + } catch (ignored: Exception) { + defaultValue + } + } + + /** + * @return the value of the Java system property with the specified `property`, while falling back to the + * specified default value if the property access fails. + */ + fun getBoolean(property: String, defaultValue: Boolean): Boolean { + var value = getProperty(property) ?: return defaultValue + value = value.trim { it <= ' ' }.lowercase(Locale.getDefault()) + if (value.isEmpty()) { + return defaultValue + } + if ("false" == value || "no" == value || "0" == value) { + return false + } + return if ("true" == value || "yes" == value || "1" == value) { + true + } else defaultValue + } + + /** + * @return the value of the Java system property with the specified `property`, while falling back to the + * specified default value if the property access fails. + */ + fun getInt(property: String, defaultValue: Int): Int { + var value = getProperty(property) ?: return defaultValue + value = value.trim { it <= ' ' } + try { + return value.toInt() + } catch (ignored: Exception) { + } + return defaultValue + } + + /** + * @return the value of the Java system property with the specified `property`, while falling back to the + * specified default value if the property access fails. + */ + fun getLong(property: String, defaultValue: Long): Long { + var value = getProperty(property) ?: return defaultValue + value = value.trim { it <= ' ' } + try { + return value.toLong() + } catch (ignored: Exception) { + } + return defaultValue + } + + /** + * @return the value of the Java system property with the specified `property`, while falling back to the + * specified default value if the property access fails. + */ + fun getFloat(property: String, defaultValue: Float): Float { + var value = getProperty(property) ?: return defaultValue + value = value.trim { it <= ' ' } + try { + return value.toFloat() + } catch (ignored: Exception) { + } + return defaultValue + } + + /** + * @return the value of the Java system property with the specified `property`, while falling back to the + * specified default value if the property access fails. + */ + fun getDouble(property: String, defaultValue: Double): Double { + var value = getProperty(property) ?: return defaultValue + value = value.trim { it <= ' ' } + try { + return value.toDouble() + } catch (ignored: Exception) { + } + return defaultValue + } + + fun getColor(property: String, defaultValue: Color): Color { + var value = getProperty(property) ?: return defaultValue + value = value.trim { it <= ' ' } + try { + return Color.decode(value) + } catch (ignored: Exception) { + } + return defaultValue + } + + /** + * @return the OS Type that is running on this machine + */ + fun get(): OSType? { + return osType + } + + fun is64bit(): Boolean { + return osType!!.is64bit + } + + fun is32bit(): Boolean { + return osType!!.is32bit + } + + /** + * @return true if this is a "standard" x86/x64 architecture (intel/amd/etc) processor. + */ + val isX86: Boolean + get() = osType!!.isX86 + val isMips: Boolean + get() = osType!!.isMips + val isArm: Boolean + get() = osType!!.isArm + val isLinux: Boolean + get() = osType!!.isLinux + val isUnix: Boolean + get() = osType!!.isUnix + val isSolaris: Boolean + get() = osType!!.isSolaris + val isWindows: Boolean + get() = osType!!.isWindows + val isMacOsX: Boolean + get() = osType!!.isMacOsX + val isAndroid: Boolean + get() = osType!!.isAndroid + + + /** + * Gets the currently running MAJOR java version as a NUMBER. For example, "Java version 1.7u45", and converts it into 7, uses JEP 223 for java > 9 + */ + private fun _getJavaVersion(): Int { + // this should never be a problem, but just in case + var fullJavaVersion = getProperty("java.version", "") + if (fullJavaVersion!!.startsWith("1.")) { + when (fullJavaVersion[2]) { + '4' -> return 4 + '5' -> return 5 + '6' -> return 6 + '7' -> return 7 + '8' -> return 8 + '9' -> return 9 + } + } else { + // We are >= java 10, use JEP 223 to get the version (early releases of 9 might not have JEP 223, so 10 is guaranteed to have it) + fullJavaVersion = getProperty("java.specification.version", "10") + try { + // it will ALWAYS be the major release version as an integer. See http://openjdk.java.net/jeps/223 + return fullJavaVersion!!.toInt() + } catch (ignored: Exception) { + } + } + + // the last valid guess we have, since the current Java implementation, whatever it is, decided not to cooperate with JEP 223. + return 10 + } + + /** + * Set our system to UTC time zone. Retrieve the **original** time zone via [.getOriginalTimeZone] + */ + fun setUTC() { + // have to set our default timezone to UTC. EVERYTHING will be UTC, and if we want local, we must explicitly ask for it. + TimeZone.setDefault(TimeZone.getTimeZone("UTC")) + } + + /** + * @return the optimum number of threads for a given task. Makes certain not to take ALL the threads, always returns at least one + * thread. + */ + val optimumNumberOfThreads: Int + get() = (Runtime.getRuntime().availableProcessors() - 2).coerceAtLeast(1) + + /** + * @return the first line of the exception message from 'throwable', or the type if there was no message. + */ + fun getExceptionMessage(throwable: Throwable): String? { + var message = throwable.message + if (message != null) { + val index = message.indexOf(LINE_SEPARATOR) + if (index > -1) { + message = message.substring(0, index) + } + } else { + message = throwable.javaClass.simpleName + } + return message + } +} diff --git a/src/dorkbox/os/OSType.java b/src/dorkbox/os/OSType.java deleted file mode 100644 index aa7f218..0000000 --- a/src/dorkbox/os/OSType.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2010 dorkbox, llc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dorkbox.os; - -public enum OSType { - Windows32("windows_32", ".dll"), - Windows64("windows_64", ".dll"), - Linux32("linux_32", ".so"), - Linux64("linux_64", ".so"), - MacOsX32("macosx_32", ".jnilib", ".dylib"), - MacOsX64("macosx_64", ".jnilib", ".dylib"), - - UnixArm("unix_arm", ".so"), - Unix32("unix_32", ".so"), - Unix64("unix_64", ".so"), - - Solaris("solaris", ".so"), - - AndroidArm56("android_arm56", ".so"), // 32bit no hardware float support - AndroidArm7("android_arm7", ".so"), // 32bit hardware float support - AndroidArm8("android_arm8", ".so"), // 64bit (w/ hardware float. everything now has hard float) - - AndroidMips("android_mips", ".so"), // 32bit mips - AndroidX86("android_x86", ".so"), // 32bit x86 (usually emulator) - - AndroidMips64("android_mips64", ".so"), // 64bit mips - AndroidX86_64("android_x86_64", ".so"), // 64bit x86 (usually emulator) - - /* - * Linux OS, Hard float, meaning floats are handled in hardware. WE ONLY SUPPORT HARD FLOATS for linux ARM!. - * For Raspberry-PI, Beaglebone, Odroid, etc PCs - */ - LinuxArm32("linux_arm7_hf", ".so"), - LinuxArm64("linux_arm8_hf", ".so"), - ; - - private final String name; - private final String[] libraryNames; - - OSType(String name, String... libraryNames) { - this.name = name; - this.libraryNames = libraryNames; - } - - public String getName() { - return this.name; - } - public String[] getLibraryNames() { - return this.libraryNames; - } - - - public - boolean is64bit() { - return this == OSType.Linux64 || this == OSType.LinuxArm64 || - this == OSType.Windows64 || this == OSType.MacOsX64 || - this == OSType.AndroidArm8 || this == OSType.AndroidX86_64 || this == OSType.AndroidMips64 || - this == OSType.Unix64; - } - - public - boolean is32bit() { - return this == OSType.Linux32 || this == OSType.LinuxArm32 || - this == OSType.Windows32 || this == OSType.MacOsX32 || - this == OSType.AndroidArm56 || this == OSType.AndroidArm7 || this == OSType.AndroidX86 || this == OSType.AndroidMips || - this == OSType.UnixArm || this == OSType.Unix32; - } - - public - boolean isMips() { - return this == OSType.AndroidMips || this == OSType.AndroidMips64; - } - - /** - * @return true if this is a "standard" x86/x64 architecture (intel/amd/etc) processor. - */ - public - boolean isX86() { - return this == OSType.Linux64 || this == OSType.LinuxArm64 || - this == OSType.Windows64 || this == OSType.MacOsX64 || - this == OSType.Linux32 || this == OSType.LinuxArm32 || - this == OSType.Windows32 || this == OSType.MacOsX32 || - this == OSType.Unix32 || this == OSType.Unix64 || - this == OSType.AndroidX86 || this == OSType.AndroidX86_64; - } - - public - boolean isArm() { - return this == OSType.LinuxArm32 || this == OSType.LinuxArm64 || - this == OSType.AndroidArm56 || this == OSType.AndroidArm7 || this == OSType.AndroidArm8; - } - - public - boolean isLinux() { - return this == OSType.Linux32 || this == OSType.Linux64 || this == OSType.LinuxArm64 || this == OSType.LinuxArm32; - } - - public - boolean isUnix() { - return this == OSType.Unix32 || this == OSType.Unix64 || this == OSType.UnixArm; - } - - public - boolean isSolaris() { - return this == OSType.Solaris; - } - - public - boolean isWindows() { - return this == OSType.Windows64 || this == OSType.Windows32; - } - - public - boolean isMacOsX() { - return this == OSType.MacOsX64 || this == OSType.MacOsX32; - } - - public - boolean isAndroid() { - return this == OSType.AndroidArm56 || this == OSType.AndroidArm7 || this == OSType.AndroidX86 || this == OSType.AndroidMips || - this == OSType.AndroidArm8 || this == OSType.AndroidX86_64 || this == OSType.AndroidMips64; - } -} diff --git a/src/dorkbox/os/OSType.kt b/src/dorkbox/os/OSType.kt new file mode 100644 index 0000000..48c65b8 --- /dev/null +++ b/src/dorkbox/os/OSType.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2010 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.os + +enum class OSType(name: String, vararg libraryNames: String) { + Windows32("windows_32", ".dll"), + Windows64("windows_64", ".dll"), + Linux32("linux_32", ".so"), + Linux64("linux_64", ".so"), + + MacOsX32("macosx_32", ".jnilib", ".dylib"), + MacOsX64("macosx_64", ".jnilib", ".dylib"), + UnixArm("unix_arm", ".so"), + Unix32("unix_32",".so"), + + Unix64("unix_64", ".so"), + Solaris("solaris", ".so"), + AndroidArm56("android_arm56", ".so"), // 32bit no hardware float support + AndroidArm7("android_arm7", ".so"), // 32bit hardware float support + AndroidArm8("android_arm8", ".so"), // 64bit (w/ hardware float. everything now has hard float) + AndroidMips("android_mips", ".so"), // 32bit mips + AndroidX86("android_x86", ".so"), // 32bit x86 (usually emulator) + AndroidMips64("android_mips64", ".so"), // 64bit mips + AndroidX86_64("android_x86_64", ".so"), // 64bit x86 (usually emulator) + + /* + * Linux OS, Hard float, meaning floats are handled in hardware. WE ONLY SUPPORT HARD FLOATS for linux ARM!. + * For Raspberry-PI, Beaglebone, Odroid, etc PCs + */ + LinuxArm32("linux_arm7_hf", ".so"), + LinuxArm64("linux_arm8_hf", ".so"); + + + val libraryNames: Array + + init { + this.libraryNames = libraryNames + } + + val is64bit: Boolean + get() { + return this == Linux64 || this == LinuxArm64 || this == Windows64 || this == MacOsX64 || this == AndroidArm8 || this == AndroidX86_64 || this == AndroidMips64 || this == Unix64 + } + + val is32bit: Boolean + get() { + return this == Linux32 || this == LinuxArm32 || this == Windows32 || this == MacOsX32 || this == AndroidArm56 || this == AndroidArm7 || this == AndroidX86 || this == AndroidMips || this == UnixArm || this == Unix32 + } + + val isMips: Boolean + get() = this == AndroidMips || this == AndroidMips64 + + /** + * @return true if this is a "standard" x86/x64 architecture (intel/amd/etc) processor. + */ + val isX86: Boolean + get() = this == Linux64 || this == LinuxArm64 || this == Windows64 || this == MacOsX64 || this == Linux32 || this == LinuxArm32 || this == Windows32 || this == MacOsX32 || this == Unix32 || this == Unix64 || this == AndroidX86 || this == AndroidX86_64 + val isArm: Boolean + get() = this == LinuxArm32 || this == LinuxArm64 || this == AndroidArm56 || this == AndroidArm7 || this == AndroidArm8 + val isLinux: Boolean + get() = this == Linux32 || this == Linux64 || this == LinuxArm64 || this == LinuxArm32 + val isUnix: Boolean + get() = this == Unix32 || this == Unix64 || this == UnixArm + val isSolaris: Boolean + get() = this == Solaris + val isWindows: Boolean + get() = this == Windows64 || this == Windows32 + val isMacOsX: Boolean + get() = this == MacOsX64 || this == MacOsX32 + val isAndroid: Boolean + get() = this == AndroidArm56 || this == AndroidArm7 || this == AndroidX86 || this == AndroidMips || this == AndroidArm8 || this == AndroidX86_64 || this == AndroidMips64 +} diff --git a/src/dorkbox/os/OSUtil.java b/src/dorkbox/os/OSUtil.java index 83a7792..869b5aa 100644 --- a/src/dorkbox/os/OSUtil.java +++ b/src/dorkbox/os/OSUtil.java @@ -81,7 +81,7 @@ class OSUtil { int[] getVersion() { int[] version = new int[2]; - if (!OS.isWindows()) { + if (!OS.INSTANCE.isWindows()) { return version; } @@ -179,7 +179,7 @@ class OSUtil { class Unix { public static boolean isFreeBSD() { - if (!OS.isUnix()) { + if (!OS.INSTANCE.isUnix()) { return false; } @@ -207,7 +207,7 @@ class OSUtil { return info; } - if (!OS.isLinux()) { + if (!OS.INSTANCE.isLinux()) { info = ""; return info; } @@ -751,7 +751,7 @@ class OSUtil { private static volatile Boolean isMATE = null; public static boolean isMATE() { - if (!OS.isLinux() && !OS.isUnix()) { + if (!OS.INSTANCE.isLinux() && !OS.INSTANCE.isUnix()) { return false; } @@ -772,7 +772,7 @@ class OSUtil { private static volatile Boolean isGnome = null; public static boolean isGnome() { - if (!OS.isLinux() && !OS.isUnix()) { + if (!OS.INSTANCE.isLinux() && !OS.INSTANCE.isUnix()) { return false; } @@ -787,7 +787,7 @@ class OSUtil { // ps x | grep gnome-shell boolean contains = Executor.Companion.run("ps", "x").contains("gnome-shell"); - if (!contains && OS.isLinux()) { + if (!contains && OS.INSTANCE.isLinux()) { // only try again if we are linux // ps a | grep gnome-shell @@ -814,7 +814,7 @@ class OSUtil { return gnomeVersion; } - if (!OS.isLinux() && !OS.isUnix()) { + if (!OS.INSTANCE.isLinux() && !OS.INSTANCE.isUnix()) { return null; } @@ -903,7 +903,7 @@ class OSUtil { return getPlasmaVersionFull; } - if (!OS.isLinux() && !OS.isUnix()) { + if (!OS.INSTANCE.isLinux() && !OS.INSTANCE.isUnix()) { return null; } @@ -932,7 +932,7 @@ class OSUtil { private static volatile Boolean isXfce = null; public static boolean isXfce() { - if (!OS.isLinux() && !OS.isUnix()) { + if (!OS.INSTANCE.isLinux() && !OS.INSTANCE.isUnix()) { return false; } @@ -947,7 +947,7 @@ class OSUtil { // ps x | grep xfce boolean contains = Executor.Companion.run("ps", "x").contains("xfce"); - if (!contains && OS.isLinux()) { + if (!contains && OS.INSTANCE.isLinux()) { // only try again if we are linux // ps a | grep gnome-shell @@ -977,7 +977,7 @@ class OSUtil { return isNautilus; } - if (!OS.isLinux() && !OS.isUnix()) { + if (!OS.INSTANCE.isLinux() && !OS.INSTANCE.isUnix()) { isNautilus = false; return false; } @@ -1006,7 +1006,7 @@ class OSUtil { public static boolean isChromeOS() { if (isChromeOS == null) { - if (!OS.isLinux()) { + if (!OS.INSTANCE.isLinux()) { isChromeOS = false; return false; } @@ -1037,7 +1037,7 @@ class OSUtil { */ public static String queryXfce(String channel, String property) { - if (!OS.isLinux() && !OS.isUnix()) { + if (!OS.INSTANCE.isLinux() && !OS.INSTANCE.isUnix()) { return ""; } diff --git a/src/dorkbox/util/CacheUtil.java b/src/dorkbox/util/CacheUtil.java index c697028..a7f700a 100644 --- a/src/dorkbox/util/CacheUtil.java +++ b/src/dorkbox/util/CacheUtil.java @@ -23,6 +23,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.math.BigInteger; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Locale; @@ -62,7 +63,7 @@ class CacheUtil { void clear() { // deletes all of the files (recursively) in the specified location. If the directory is empty (no locked files), then the // directory is also deleted. - FileUtil.delete(new File(OS.TEMP_DIR, tempDir)); + FileUtil.delete(new File(OS.INSTANCE.TEMP_DIR, tempDir)); } @@ -387,11 +388,11 @@ class CacheUtil { throw new NullPointerException("cacheName"); } - File saveDir = new File(OS.TEMP_DIR, tempDir); + File saveDir = new File(OS.INSTANCE.TEMP_DIR, tempDir); // can be wimpy, only one at a time String hash = hashName(cacheName); - String extension = FileUtil.getExtension(cacheName); + String extension = FileUtil.INSTANCE.getExtension(cacheName); if (extension.isEmpty()) { extension = "cache"; } @@ -408,7 +409,7 @@ class CacheUtil { private static String hashName(final String name) { // figure out the fileName - byte[] bytes = name.getBytes(OS.UTF_8); + byte[] bytes = name.getBytes(StandardCharsets.UTF_8); MessageDigest digest = digestLocal.get(); digest.reset(); diff --git a/src/dorkbox/util/CommonUtils.kt b/src/dorkbox/util/CommonUtils.kt new file mode 100644 index 0000000..811568e --- /dev/null +++ b/src/dorkbox/util/CommonUtils.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2017 Pronghorn Technology LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dorkbox.util + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import mu.KLogger +import mu.KotlinLogging +import java.io.PrintWriter +import java.io.StringWriter +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.coroutineContext + +inline fun T.logger(name: String): KLogger { + return KotlinLogging.logger(name) +} + +inline fun T.logger(): KLogger { + if (T::class.isCompanion) { + return KotlinLogging.logger(T::class.java.enclosingClass.simpleName) + } + return KotlinLogging.logger(T::class.java.simpleName) +} + + +fun Exception.stackTraceToString(): String { + val exceptionWriter = StringWriter() + printStackTrace(PrintWriter(exceptionWriter)) + return exceptionWriter.toString() +} + +inline fun ignoreException(block: () -> Unit) { + try { + block() + } catch (ex: Exception) { + // no-op + } +} + +@Suppress("NOTHING_TO_INLINE") +inline fun ignoreExceptions(vararg blocks: () -> Unit) { + blocks.forEach { block -> + try { + block() + } catch (ex: Exception) { + // no-op + } + } +} + +fun async(dispatcher: CoroutineDispatcher = Dispatchers.IO, action: suspend CoroutineScope.() -> Unit): Job { + return GlobalScope.launch(dispatcher) { + action() + } +} + +suspend fun Mutex.withReentrantLock(block: suspend () -> T): T { + val key = ReentrantMutexContextKey(this) + + // call block directly when this mutex is already locked in the context + if (coroutineContext[key] != null) return block() + + // otherwise add it to the context and lock the mutex + return withContext(ReentrantMutexContextElement(key)) { + withLock { block() } + } +} + +class ReentrantMutexContextElement(override val key: ReentrantMutexContextKey) : CoroutineContext.Element +data class ReentrantMutexContextKey(val mutex: Mutex) : CoroutineContext.Key diff --git a/src/dorkbox/util/Desktop.java b/src/dorkbox/util/Desktop.java index 0ef36ba..bb86cd8 100644 --- a/src/dorkbox/util/Desktop.java +++ b/src/dorkbox/util/Desktop.java @@ -236,7 +236,7 @@ class Desktop { throw new IOException("Path must not be null or empty."); } - if (OS.isMacOsX()) { + if (OS.INSTANCE.isMacOsX()) { File directory = new File(path); // Mac tries to open the .app rather than browsing it. Instead, pass a child with -R to select it in finder @@ -285,7 +285,7 @@ class Desktop { * - CLI has thread/memory overhead */ private static boolean requireUnixLauncher() { - return ((OS.isUnix() || OS.isLinux()) && (GtkCheck.isGtkLoaded && GtkCheck.isGtk3)); + return ((OS.INSTANCE.isUnix() || OS.INSTANCE.isLinux()) && (GtkCheck.isGtkLoaded && GtkCheck.isGtk3)); } /** diff --git a/src/dorkbox/util/DomainUtils.kt b/src/dorkbox/util/DomainUtils.kt new file mode 100644 index 0000000..54d3986 --- /dev/null +++ b/src/dorkbox/util/DomainUtils.kt @@ -0,0 +1,210 @@ +/* + * Copyright 2013 dorkbox, llc + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * Derivative code has been released as Apache 2.0, used with permission. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.util + +import java.io.File +import java.util.* + +/** + * And the effective_tld_names.dat is from mozilla (the following are all the same data) + * + * + * https://mxr.mozilla.org/mozilla-central/source/netwerk/dns/effective_tld_names.dat?raw=1 + * which is... + * https://publicsuffix.org/list/effective_tld_names.dat + * + * + * also + * + * + * https://publicsuffix.org/list/public_suffix_list.dat + */ +object DomainUtils { + private val exceptions = HashSet() + private val suffixes = HashSet() + + fun init() { + // just here to load the class. + } + + init { + val tldFileName = "effective_tld_names.dat.txt" + + /* + * Parses the list from publicsuffix.org + * Copied from + * http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/httpclient/src/main/java/org/apache/http/impl/cookie/PublicSuffixListParser.java + * + * new one at: + * http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/cookie/PublicSuffixDomainFilter.java + * and + * http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/psl/ + */ + + // now load this file into memory, so it's faster to process. + var file = File("blacklist", tldFileName) + val paths = LinkedList(Arrays.asList("NetRefDependencies", "..")) + + while (!file.canRead() && !paths.isEmpty()) { + // for work in an IDE. Path can vary, so we work our way up + file = File(paths.removeFirst(), file.toString()) + } + + file = file.absoluteFile + if (!file.canRead()) { + throw RuntimeException("Unable to load the TLD list: $tldFileName") + } + + FileUtil.read(file, object : FileUtil.Action { + override fun onLineRead(line: String) { + var line = line + + // entire lines can also be commented using // + if (!line.isEmpty() && !line.startsWith("//")) { + + if (line.startsWith(".")) { + line = line.substring(1) // A leading dot is optional + } + + // An exclamation mark (!) at the start of a rule marks an exception + // to a previous wildcard rule + val isException = line.startsWith("!") + if (isException) { + line = line.substring(1) + } + + if (isException) { + exceptions.add(line) + } else { + suffixes.add(line) + } + } + } + + override fun finished() {} + }) + + } + + /** + * Extracts the second level domain, from a fully qualified domain (ie: www.aa.com, or www.amazon.co.uk). + * + * + * This algorithm works from left to right parsing the domain string parameter + * + * @param domain a fully qualified domain (ie: www.aa.com, or www.amazon.co.uk) + * + * @return null (if there is no second level domain) or the SLD www.aa.com -> aa.com , or www.amazon.co.uk -> amazon.co.uk + */ + fun extractSLD(domain: String): String? { + var domain = domain + var last = domain + var anySLD = false + + do { + if (isTLD(domain)) { + return if (anySLD) { + last + } + else { + null + } + } + + anySLD = true + last = domain + + val nextDot = domain.indexOf(".") + if (nextDot == -1) { + return null + } + + domain = domain.substring(nextDot + 1) + } while (domain.isNotEmpty()) + + return null + } + + /** + * Returns a domain that is without it's TLD at the end. + * + * @param domain domain a fully qualified domain or not, (ie: www.aa.com, or amazon.co.uk). + * + * @return a domain that is without it's TLD, ie: www.aa.com -> www.aa, or google.com -> google + */ + fun withoutTLD(domain: String): String? { + + var index = 0 + while (index != -1) { + index = domain.indexOf('.', index) + + if (index != -1) { + if (isTLD(domain.substring(index))) { + return domain.substring(0, index) + } + index++ + } + else { + return null + } + } + + return null + } + + /** + * Checks if the domain is a TLD. + */ + fun isTLD(domain: String): Boolean { + var domain = domain + if (domain.startsWith(".")) { + domain = domain.substring(1) + } + + // An exception rule takes priority over any other matching rule. + // Exceptions are ones that are not a TLD, but would match a pattern rule + // e.g. bl.uk is not a TLD, but the rule *.uk means it is. Hence there is an exception rule + // stating that bl.uk is not a TLD. + if (exceptions.contains(domain)) { + return false + } + + if (suffixes.contains(domain)) { + return true + } + + // Try patterns. ie *.jp means that boo.jp is a TLD + val nextdot = domain.indexOf('.') + if (nextdot == -1) { + return false + } + domain = "*" + domain.substring(nextdot) + + return suffixes.contains(domain) + + } +// +// @JvmStatic +// fun main(args: Array) { +// System.err.println("isTLD(espn.com) = " + isTLD("espn.com")) +// System.err.println("withoutTLD(com) = " + withoutTLD("com")) +// System.err.println("withoutTLD(chrome:extension) = " + withoutTLD("")) +// } +} diff --git a/src/dorkbox/util/FileUtil.java b/src/dorkbox/util/FileUtil.java deleted file mode 100644 index 3a4ffec..0000000 --- a/src/dorkbox/util/FileUtil.java +++ /dev/null @@ -1,1990 +0,0 @@ -/* - * Copyright 2010 dorkbox, llc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dorkbox.util; - -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.RandomAccessFile; -import java.io.Reader; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import dorkbox.os.OS; - -/** - * File related utilities. - *

- *

- * Contains code from FilenameUtils.java (normalize + dependencies) - Apache 2.0 License - * http://commons.apache.org/proper/commons-io/ - * Copyright 2013 ASF - * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, - * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, - * Jeremias Maerki, Stephen Colebourne - */ -@SuppressWarnings({"WeakerAccess", "Duplicates", "unused"}) -public -class FileUtil { - private static final boolean DEBUG = false; - - /** - * The Unix separator character. - */ - private static final char UNIX_SEPARATOR = '/'; - - /** - * The Windows separator character. - */ - private static final char WINDOWS_SEPARATOR = '\\'; - - /** - * The system separator character. - */ - private static final char SYSTEM_SEPARATOR = File.separatorChar; - - /** - * The separator character that is the opposite of the system separator. - */ - private static final char OTHER_SEPARATOR; - - static { - if (OS.isWindows()) { - OTHER_SEPARATOR = UNIX_SEPARATOR; - } - else { - OTHER_SEPARATOR = WINDOWS_SEPARATOR; - } - } - - - public static byte[] ZIP_HEADER = {'P', 'K', (byte) 0x3, (byte) 0x4}; - - /** - * Converts the content of a file into a list of strings. Lines are trimmed. - * - * @param file the input file to read. Throws an error if this file cannot be read. - * @param includeEmptyLines true if you want the resulting list of String to include blank/empty lines from the file - * - * @return A list of strings, one line per string, of the content - */ - public static - List read(final File file, final boolean includeEmptyLines) throws IOException { - List lines = new ArrayList(); - FileReader fileReader = new FileReader(file); - try { - BufferedReader bin = new BufferedReader(fileReader); - String line; - - if (includeEmptyLines) { - while (( line = bin.readLine()) != null) { - lines.add(line); - } - } else { - while ((line = bin.readLine()) != null) { - if (!line.isEmpty()) { - lines.add(line); - } - } - } - } finally { - IO.closeQuietly(fileReader); - } - return lines; - } - - /** - * Convenience method that converts the content of a file into a giant string. - * - * @param file the input file to read. Throws an error if this file cannot be read. - * - * @return A string, matching the contents of the file - */ - public static - String readAsString(final File file) throws IOException { - StringBuilder stringBuilder = new StringBuilder((int) (file.length())); - read(file, stringBuilder); - - return stringBuilder.toString(); - } - - /** - * Writes the content of a file to the passed in StringBuilder. - * - * @param file the input file to read. Throws an error if this file cannot be read. - * @param stringBuilder the stringBuilder this file will be written to - */ - public static - void read(final File file, final StringBuilder stringBuilder) throws IOException { - FileReader fileReader = new FileReader(file); - try { - BufferedReader bin = new BufferedReader(fileReader); - String line; - - while (( line = bin.readLine()) != null) { - stringBuilder.append(line).append(OS.LINE_SEPARATOR); - } - } finally { - IO.closeQuietly(fileReader); - } - } - - /** - * Reads the contents of the supplied input stream into a list of lines. - * - * @return Always returns a list, even if the file does not exist, or there are errors reading it. - */ - public static - List readLines(final File file) { - FileReader fileReader = null; - try { - fileReader = new FileReader(file); - } catch (FileNotFoundException ignored) { - return new ArrayList(); - } - return readLines(fileReader); - } - - /** - * Reads the contents of the supplied input stream into a list of lines. - *

- * Closes the reader on successful or failed completion. - * - * @return Always returns a list, even if the file does not exist, or there are errors reading it. - */ - public static - List readLines(Reader in) { - List lines = new ArrayList(); - try { - BufferedReader bin = new BufferedReader(in); - String line; - try { - while ((line = bin.readLine()) != null) { - lines.add(line); - } - } catch (IOException ignored) { - } - } finally { - IO.closeQuietly(in); - } - return lines; - } - - /** - * Renames a file. Windows has all sorts of problems which are worked around. - * - * @return true if successful, false otherwise - */ - public static - boolean renameTo(File source, File dest) { - // if we're on a civilized operating system we may be able to simple - // rename it - if (source.renameTo(dest)) { - return true; - } - - // fall back to trying to rename the old file out of the way, rename the - // new file into - // place and then delete the old file - if (dest.exists()) { - File temp = new File(dest.getPath() + "_old"); - if (temp.exists()) { - if (!temp.delete()) { - if (DEBUG) { - System.err.println("Failed to delete old intermediate file: " + temp); - } - - // the subsequent code will probably fail - } - } - if (dest.renameTo(temp)) { - if (source.renameTo(dest)) { - if (temp.delete()) { - if (DEBUG) { - System.err.println("Failed to delete intermediate file: " + temp); - } - } - return true; - } - } - } - - // as a last resort, try copying the old data over the new - FileInputStream fin = null; - FileOutputStream fout = null; - try { - fin = new FileInputStream(source); - fout = new FileOutputStream(dest); - IO.copyStream(fin, fout); - - IO.close(fin); - if (!source.delete()) { - if (DEBUG) { - System.err.println("Failed to delete '" + source + "' after brute force copy to '" + dest + "'."); - } - } - return true; - - } catch (IOException ioe) { - if (DEBUG) { - System.err.println("Failed to copy '" + source + "' to '" + dest + "'."); - ioe.printStackTrace(); - } - - return false; - - } finally { - IO.close(fin); - IO.close(fout); - } - } - - /** - * Copies a files from one location to another. Overwriting any existing file at the destination. - */ - public static - File copyFile(String in, File out) throws IOException { - return copyFile(new File(in), out); - } - - /** - * Copies a files from one location to another. Overwriting any existing file at the destination. - */ - public static - File copyFile(File in, String out) throws IOException { - return copyFile(in, new File(out)); - } - - /** - * Copies a files from one location to another. Overwriting any existing file at the destination. - */ - public static - File copyFile(String in, String out) throws IOException { - return copyFile(new File(in), new File(out)); - } - - /** - * Copies a files from one location to another. Overwriting any existing file at the destination. - */ - public static - File copyFileToDir(String in, String out) throws IOException { - return copyFileToDir(new File(in), new File(out)); - } - - /** - * Copies a files from one location to another. Overwriting any existing file at the destination. - * If the out file is a directory, then the in file will be copied to the directory - */ - public static - File copyFileToDir(File in, File out) throws IOException { - if (in == null) { - throw new IllegalArgumentException("in cannot be null."); - } - if (out == null) { - throw new IllegalArgumentException("out cannot be null."); - } - - // copy the file to the directory instead - if (!out.isDirectory()) { - throw new IOException("Out file is not a directory! '" + out.getAbsolutePath() + "'"); - } - - return copyFile(in, new File(out, in.getName())); - } - - /** - * Copies a files from one location to another. Overwriting any existing file at the destination. - */ - public static - File copyFile(File in, File out) throws IOException { - if (in == null) { - throw new IllegalArgumentException("in cannot be null."); - } - if (out == null) { - throw new IllegalArgumentException("out cannot be null."); - } - - - String normalizedIn = normalize(in).getAbsolutePath(); - String normalizedout = normalize(out).getAbsolutePath(); - - if (normalizedIn.equalsIgnoreCase(normalizedout)) { - if (DEBUG) { - System.err.println("Source equals destination! " + normalizedIn); - } - return out; - } - - - // if out doesn't exist, then create it. - File parentOut = out.getParentFile(); - if (!parentOut.canWrite()) { - //noinspection ResultOfMethodCallIgnored - parentOut.mkdirs(); - } - - if (DEBUG) { - System.err.println("Copying file: '" + in + "' --> '" + out + "'"); - } - - FileChannel sourceChannel = null; - FileChannel destinationChannel = null; - try { - sourceChannel = new FileInputStream(normalizedIn).getChannel(); - destinationChannel = new FileOutputStream(normalizedout).getChannel(); - - if (sourceChannel.size() == 0) { - System.err.println("Source size is ZERO: " + normalizedIn); - } - - sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel); - } finally { - try { - if (sourceChannel != null) { - sourceChannel.close(); - } - } catch (Exception ignored) { - } - try { - if (destinationChannel != null) { - destinationChannel.close(); - } - } catch (Exception ignored) { - } - } - - //noinspection ResultOfMethodCallIgnored - out.setLastModified(in.lastModified()); - - return out; - } - - /** - * Copies the contents of file two onto the END of file one. - */ - public static - File concatFiles(File one, File two) { - if (one == null) { - throw new IllegalArgumentException("one cannot be null."); - } - if (two == null) { - throw new IllegalArgumentException("two cannot be null."); - } - - String normalizedOne = normalize(one).getAbsolutePath(); - String normalizedTwo = normalize(two).getAbsolutePath(); - - if (DEBUG) { - System.err.println("Concat'ing file: '" + one + "' --> '" + two + "'"); - } - - FileChannel channelOne = null; - FileChannel channelTwo = null; - try { - // open it in append mode - channelOne = new FileOutputStream(normalizedOne, true).getChannel(); - channelTwo = new FileInputStream(normalizedTwo).getChannel(); - - long size = two.length(); - while (size > 0) { - size -= channelOne.transferFrom(channelTwo, 0, size); - } - } catch (Exception ignored) { - ignored.printStackTrace(); - } finally { - try { - if (channelOne != null) { - channelOne.close(); - } - } catch (Exception ignored) { - } - try { - if (channelTwo != null) { - channelTwo.close(); - } - } catch (Exception ignored) { - } - } - - //noinspection ResultOfMethodCallIgnored - one.setLastModified(System.currentTimeMillis()); - - return one; - } - - /** - * Moves a file, overwriting any existing file at the destination. - */ - public static - File moveFile(String in, File out) throws IOException { - return moveFile(new File(in), out); - } - - /** - * Moves a file, overwriting any existing file at the destination. - */ - public static - File moveFile(File in, String out) throws IOException { - return moveFile(in, new File(out)); - } - - - /** - * Moves a file, overwriting any existing file at the destination. - */ - public static - File moveFile(String in, String out) throws IOException { - return moveFile(new File(in), new File(out)); - } - - /** - * Moves a file, overwriting any existing file at the destination. - */ - public static - File moveFile(File in, File out) throws IOException { - if (in == null) { - throw new IllegalArgumentException("in cannot be null."); - } - if (out == null) { - throw new IllegalArgumentException("out cannot be null."); - } - - if (out.canRead()) { - //noinspection ResultOfMethodCallIgnored - out.delete(); - } - - boolean renameSuccess = renameTo(in, out); - if (!renameSuccess) { - throw new IOException("Unable to move file: '" + in.getAbsolutePath() + "' -> '" + out.getAbsolutePath() + "'"); - } - return out; - } - - /** - * Copies a directory from one location to another - */ - public static - void copyDirectory(String src, String dest, String... namesToIgnore) throws IOException { - copyDirectory(new File(src), new File(dest), namesToIgnore); - } - - - /** - * Copies a directory from one location to another - */ - public static - void copyDirectory(File src_, File dest_, String... namesToIgnore) throws IOException { - File src = FileUtil.normalize(src_); - File dest = FileUtil.normalize(dest_); - - if (namesToIgnore.length > 0) { - String name = src.getName(); - for (String ignore : namesToIgnore) { - if (name.equals(ignore)) { - return; - } - } - } - - - if (src.isDirectory()) { - // if directory not exists, create it - if (!dest.exists()) { - //noinspection ResultOfMethodCallIgnored - dest.mkdir(); - if (DEBUG) { - System.err.println("Directory copied from '" + src + "' --> '" + dest + "'"); - } - } - - // list all the directory contents - String files[] = src.list(); - - if (files != null) { - for (String file : files) { - // construct the src and dest file structure - File srcFile = new File(src, file); - File destFile = new File(dest, file); - - // recursive copy - copyDirectory(srcFile, destFile, namesToIgnore); - } - } - } - else { - // if file, then copy it - copyFile(src, dest); - } - } - - /** - * Safely moves a directory from one location to another (by copying it first, then deleting the original). - */ - public static - void moveDirectory(String src, String dest, String... fileNamesToIgnore) throws IOException { - moveDirectory(new File(src), new File(dest), fileNamesToIgnore); - } - - /** - * Safely moves a directory from one location to another (by copying it first, then deleting the original). - */ - public static - void moveDirectory(File src, File dest, String... fileNamesToIgnore) throws IOException { - if (fileNamesToIgnore.length > 0) { - String name = src.getName(); - for (String ignore : fileNamesToIgnore) { - if (name.equals(ignore)) { - return; - } - } - } - - - if (src.isDirectory()) { - // if directory not exists, create it - if (!dest.exists()) { - //noinspection ResultOfMethodCallIgnored - dest.mkdir(); - if (DEBUG) { - System.err.println("Directory copied from '" + src + "' --> '" + dest + "'"); - } - } - - // list all the directory contents - String files[] = src.list(); - - if (files != null) { - for (String file : files) { - // construct the src and dest file structure - File srcFile = new File(src, file); - File destFile = new File(dest, file); - - // recursive copy - moveDirectory(srcFile, destFile, fileNamesToIgnore); - } - } - } - else { - // if file, then copy it - moveFile(src, dest); - } - } - - /** - * Deletes a file or directory and all files and sub-directories under it. - * - * @param fileNamesToIgnore if prefaced with a '/', it will ignore as a directory instead of file - * @return true iff the file/dir was deleted - */ - public static - boolean delete(String fileName, String... fileNamesToIgnore) { - if (fileName == null) { - throw new IllegalArgumentException("fileName cannot be null."); - } - - return delete(new File(fileName), fileNamesToIgnore); - } - - /** - * Deletes a file, directory + all files and sub-directories under it. The directory is ALSO deleted if it because empty as a result - * of this operation - * - * @param namesToIgnore if prefaced with a '/', it will treat the name to ignore as a directory instead of file - * - * @return true IFF the file/dir was deleted or didn't exist at first - */ - public static - boolean delete(File file, String... namesToIgnore) { - if (!file.exists()) { - return true; - } - - boolean thingsDeleted = false; - boolean ignored = false; - - if (file.isDirectory()) { - File[] files = file.listFiles(); - if (files != null) { - for (int i = 0, n = files.length; i < n; i++) { - - boolean delete = true; - final File file2 = files[i]; - String name2 = file2.getName(); - String name2Full = normalize(file2).getAbsolutePath(); - - if (file2.isDirectory()) { - for (String name : namesToIgnore) { - if (name.charAt(0) == UNIX_SEPARATOR && name.equals(name2)) { - // only name match if our name To Ignore starts with a / or \ - if (DEBUG) { - System.err.println("Skipping delete dir: " + file2); - } - ignored = true; - delete = false; - break; - } - else if (name.equals(name2Full)) { - // full path match - if (DEBUG) { - System.err.println("Skipping delete dir: " + file2); - } - ignored = true; - delete = false; - break; - } - } - - if (delete) { - if (DEBUG) { - System.err.println("Deleting dir: " + file2); - } - delete(file2, namesToIgnore); - } - } - else { - for (String name : namesToIgnore) { - if (name.charAt(0) != UNIX_SEPARATOR && name.equals(name2)) { - // only name match - if (DEBUG) { - System.err.println("Skipping delete file: " + file2); - } - ignored = true; - delete = false; - break; - } - else if (name.equals(name2Full)) { - // full path match - if (DEBUG) { - System.err.println("Skipping delete file: " + file2); - } - ignored = true; - delete = false; - break; - } - } - - if (delete) { - if (DEBUG) { - System.err.println("Deleting file: " + file2); - } - thingsDeleted |= file2.delete(); - } - } - } - } - } - - // don't try to delete the dir if there was an ignored file in it - if (ignored) { - if (DEBUG) { - System.err.println("Skipping deleting file: " + file); - } - return false; - } - - if (DEBUG) { - System.err.println("Deleting file: " + file); - } - - thingsDeleted |= file.delete(); - return thingsDeleted; - } - - /** - * @return the contents of the file as a byte array - */ - public static - byte[] toBytes(final File file) { - FileInputStream fileInputStream = null; - try { - fileInputStream = new FileInputStream(file.getAbsolutePath()); - ByteArrayOutputStream byteArrayOutputStream = IO.copyStream(fileInputStream); - return byteArrayOutputStream.toByteArray(); - } catch (IOException ignored) { - } finally { - if (fileInputStream != null) { - IO.close(fileInputStream); - } - } - - return null; - } - - /** - * Creates the directories in the specified location. - */ - public static - String mkdir(File location) { - if (location == null) { - throw new IllegalArgumentException("fileDir cannot be null."); - } - - String path = normalize(location).getAbsolutePath(); - if (location.mkdirs()) { - if (DEBUG) { - System.err.println("Created directory: " + path); - } - } - - return path; - } - - /** - * Creates the directories in the specified location. - */ - public static - String mkdir(String location) { - if (location == null) { - throw new IllegalArgumentException("path cannot be null."); - } - - return mkdir(new File(location)); - } - - - /** - * Creates a temp file - */ - public static - File tempFile(String fileName) throws IOException { - if (fileName == null) { - throw new IllegalArgumentException("fileName cannot be null"); - } - - return normalize(File.createTempFile(fileName, null)).getAbsoluteFile(); - } - - /** - * Creates a temp directory - */ - public static - String tempDirectory(String directoryName) throws IOException { - if (directoryName == null) { - throw new IllegalArgumentException("directoryName cannot be null"); - } - - File file = File.createTempFile(directoryName, null); - if (!file.delete()) { - throw new IOException("Unable to delete temp file: " + file); - } - - if (!file.mkdir()) { - throw new IOException("Unable to create temp directory: " + file); - } - - return normalize(file).getAbsolutePath(); - } - - /** - * @return true if the inputStream is a zip/jar stream. DOES NOT CLOSE THE STREAM - */ - public static - boolean isZipStream(InputStream in) { - if (!in.markSupported()) { - in = new BufferedInputStream(in); - } - boolean isZip = true; - try { - in.mark(ZIP_HEADER.length); - for (int i = 0; i < ZIP_HEADER.length; i++) { - //noinspection NumericCastThatLosesPrecision - if (ZIP_HEADER[i] != (byte) in.read()) { - isZip = false; - break; - } - } - in.reset(); - } catch (Exception e) { - isZip = false; - } - - return isZip; - } - - /** - * @return true if the named file is a zip/jar file - */ - public static - boolean isZipFile(String fileName) { - if (fileName == null) { - throw new IllegalArgumentException("fileName cannot be null"); - } - - return isZipFile(new File(fileName)); - } - - /** - * @return true if the file is a zip/jar file - */ - public static - boolean isZipFile(File file) { - boolean isZip = true; - byte[] buffer = new byte[ZIP_HEADER.length]; - - RandomAccessFile raf = null; - try { - raf = new RandomAccessFile(file, "r"); - raf.readFully(buffer); - for (int i = 0; i < ZIP_HEADER.length; i++) { - if (buffer[i] != ZIP_HEADER[i]) { - isZip = false; - break; - } - } - } catch (Exception e) { - isZip = false; - if (e instanceof FileNotFoundException) { - e.printStackTrace(); - } - } finally { - if (raf != null) { - try { - raf.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - return isZip; - } - - - /** - * Unzips a ZIP file - */ - public static - void unzip(String zipFile, String outputDir) throws IOException { - unzipJar(zipFile, outputDir, true); - } - - /** - * Unzips a ZIP file - */ - public static - void unzip(File zipFile, File outputDir) throws IOException { - unzipJar(zipFile, outputDir, true); - } - - /** - * Unzips a ZIP file. Will close the input stream. - */ - public static - void unzip(ZipInputStream inputStream, String outputDir) throws IOException { - if (outputDir == null) { - throw new IllegalArgumentException("outputDir cannot be null."); - } - - unzip(inputStream, new File(outputDir)); - } - - /** - * Unzips a ZIP file. Will close the input stream. - */ - public static - void unzip(ZipInputStream inputStream, File outputDir) throws IOException { - unzipJar(inputStream, outputDir, true); - } - - /** - * Unzips a ZIP file - */ - public static - void unzipJar(String zipFile, String outputDir, boolean extractManifest) throws IOException { - if (zipFile == null) { - throw new IllegalArgumentException("zipFile cannot be null."); - } - if (outputDir == null) { - throw new IllegalArgumentException("outputDir cannot be null."); - } - - unjarzip0(new File(zipFile), new File(outputDir), extractManifest); - } - - /** - * Unzips a ZIP file - */ - public static - void unzipJar(File zipFile, File outputDir, boolean extractManifest) throws IOException { - if (zipFile == null) { - throw new IllegalArgumentException("zipFile cannot be null."); - } - if (outputDir == null) { - throw new IllegalArgumentException("outputDir cannot be null."); - } - - unjarzip0(zipFile, outputDir, extractManifest); - } - - /** - * Unzips a ZIP file. Will close the input stream. - */ - public static - void unzipJar(ZipInputStream inputStream, File outputDir, boolean extractManifest) throws IOException { - if (inputStream == null) { - throw new IllegalArgumentException("inputStream cannot be null."); - } - if (outputDir == null) { - throw new IllegalArgumentException("outputDir cannot be null."); - } - - unjarzip1(inputStream, outputDir, extractManifest); - } - - /** - * Unzips a ZIP or JAR file (and handles the manifest if requested) - */ - private static - void unjarzip0(File zipFile, File outputDir, boolean extractManifest) throws IOException { - if (zipFile == null) { - throw new IllegalArgumentException("zipFile cannot be null."); - } - if (outputDir == null) { - throw new IllegalArgumentException("outputDir cannot be null."); - } - - long fileLength = zipFile.length(); - if (fileLength > Integer.MAX_VALUE - 1) { - throw new RuntimeException("Source filesize is too large!"); - } - - ZipInputStream inputStream = new ZipInputStream(new FileInputStream(zipFile)); - - unjarzip1(inputStream, outputDir, extractManifest); - } - - /** - * Unzips a ZIP file - */ - private static - void unjarzip1(ZipInputStream inputStream, File outputDir, boolean extractManifest) throws IOException { - try { - ZipEntry entry; - while ((entry = inputStream.getNextEntry()) != null) { - String name = entry.getName(); - - if (!extractManifest && name.startsWith("META-INF/")) { - continue; - } - - File file = new File(outputDir, name); - if (entry.isDirectory()) { - mkdir(file.getPath()); - continue; - } - mkdir(file.getParent()); - - - FileOutputStream output = new FileOutputStream(file); - try { - IO.copyStream(inputStream, output); - } finally { - IO.close(output); - } - } - } finally { - IO.close(inputStream); - } - } - - - /** - * Parses the specified root directory for ALL files that are in it. All of the sub-directories are searched as well. - *

- * This is different, in that it returns ALL FILES, instead of ones that just match a specific extension. - * - * @return the list of all files in the root+sub-dirs. - */ - public static - List parseDir(String rootDirectory) throws IOException { - if (rootDirectory == null) { - throw new IllegalArgumentException("rootDirectory cannot be null"); - } - return parseDir(new File(rootDirectory), (String) null); - } - - /** - * Parses the specified root directory for ALL files that are in it. All of the sub-directories are searched as well. - *

- * This is different, in that it returns ALL FILES, instead of ones that just match a specific extension. - * - * @return the list of all files in the root+sub-dirs. - */ - public static - List parseDir(File rootDirectory) throws IOException { - return parseDir(rootDirectory, (String) null); - } - - /** - * Parses the specified root directory for files that end in the extension to match. All of the sub-directories are searched as well. - * - * @return the list of all files in the root+sub-dirs that match the given extension. - */ - public static - List parseDir(File rootDirectory, String... extensionsToMatch) throws IOException { - List jarList = new LinkedList(); - LinkedList directories = new LinkedList(); - - rootDirectory = FileUtil.normalize(rootDirectory); - - if (!rootDirectory.exists()) { - throw new IOException("Location does not exist: " + rootDirectory.getAbsolutePath()); - } - - if (rootDirectory.isDirectory()) { - directories.add(rootDirectory); - - while (directories.peek() != null) { - File dir = directories.poll(); - File[] listFiles = dir.listFiles(); - if (listFiles != null) { - for (File file : listFiles) { - if (file.isDirectory()) { - directories.add(file); - } - else { - if (extensionsToMatch == null || extensionsToMatch.length == 0 || extensionsToMatch[0] == null) { - jarList.add(file); - } - else { - for (String e : extensionsToMatch) { - if (file.getAbsolutePath().endsWith(e)) { - jarList.add(file); - } - } - } - } - } - } - } - } - else { - throw new IOException("Cannot search directory children if the dir is a file name: " + rootDirectory.getAbsolutePath()); - } - - - return jarList; - } - - /** - * Gets the relative path of a file to a specific directory in it's hierarchy. - *

- * For example: getChildRelativeToDir("/a/b/c/d/e.bah", "c") -> "d/e.bah" - * - * @return null if there is no child - */ - public static - String getChildRelativeToDir(String fileName, String dirInHeirarchy) { - if (fileName == null || fileName.isEmpty()) { - throw new IllegalArgumentException("fileName cannot be null."); - } - - return getChildRelativeToDir(new File(fileName), dirInHeirarchy); - } - - /** - * Gets the relative path of a file to a specific directory in it's hierarchy. - *

- * For example: getChildRelativeToDir("/a/b/c/d/e.bah", "c") -> "d/e.bah" - * - * @return null if there is no child - */ - public static - String getChildRelativeToDir(File file, String dirInHeirarchy) { - if (file == null) { - throw new IllegalArgumentException("file cannot be null."); - } - - if (dirInHeirarchy == null || dirInHeirarchy.isEmpty()) { - throw new IllegalArgumentException("dirInHeirarchy cannot be null."); - } - - String[] split = dirInHeirarchy.split(File.separator); - int splitIndex = split.length - 1; - - String absolutePath = file.getAbsolutePath(); - - File parent = file; - String parentName; - - if (splitIndex == 0) { - // match on ONE dir - while (parent != null) { - parentName = parent.getName(); - - if (parentName.equals(dirInHeirarchy)) { - parentName = parent.getAbsolutePath(); - - return absolutePath.substring(parentName.length() + 1); - } - parent = parent.getParentFile(); - } - } - else { - // match on MANY dir. They must be "in-order" - boolean matched = false; - while (parent != null) { - parentName = parent.getName(); - - if (matched) { - if (parentName.equals(split[splitIndex])) { - splitIndex--; - if (splitIndex < 0) { - // this means the ENTIRE path matched - if (absolutePath.length() == dirInHeirarchy.length()) { - return null; - } - - // +1 to account for the separator char - return absolutePath.substring(dirInHeirarchy.length() + 1, absolutePath.length()); - } - } - else { - // because it has to be "in-order", if it doesn't match, we immediately abort - return null; - } - } - else { - if (parentName.equals(split[splitIndex])) { - matched = true; - splitIndex--; - } - } - - parent = parent.getParentFile(); - } - } - - return null; - } - - /** - * Gets the PARENT relative path of a file to a specific directory in it's hierarchy. - *

- * For example: getParentRelativeToDir("/a/b/c/d/e.bah", "c") -> "/a/b" - */ - public static - String getParentRelativeToDir(String fileName, String dirInHeirarchy) { - if (fileName == null || fileName.isEmpty()) { - throw new IllegalArgumentException("fileName cannot be null."); - } - - return getParentRelativeToDir(new File(fileName), dirInHeirarchy); - } - - /** - * Gets the relative path of a file to a specific directory in it's hierarchy. - *

- * For example: getParentRelativeToDir("/a/b/c/d/e.bah", "c") -> "/a/b" - * - * @return null if it cannot be found - */ - public static - String getParentRelativeToDir(File file, String dirInHeirarchy) { - if (file == null) { - throw new IllegalArgumentException("file cannot be null."); - } - - if (dirInHeirarchy == null || dirInHeirarchy.isEmpty()) { - throw new IllegalArgumentException("dirInHeirarchy cannot be null."); - } - - String[] split = dirInHeirarchy.split(File.separator); - int splitIndex = split.length - 1; - - File parent = file; - String parentName; - - if (splitIndex == 0) { - // match on ONE dir - while (parent != null) { - parentName = parent.getName(); - - if (parentName.equals(dirInHeirarchy)) { - parent = parent.getParentFile(); - parentName = parent.getAbsolutePath(); - return parentName; - } - parent = parent.getParentFile(); - } - } - else { - // match on MANY dir. They must be "in-order" - boolean matched = false; - while (parent != null) { - parentName = parent.getName(); - - if (matched) { - if (parentName.equals(split[splitIndex])) { - splitIndex--; - if (splitIndex < 0) { - parent = parent.getParentFile(); - parentName = parent.getAbsolutePath(); - return parentName; - } - } - else { - // because it has to be "in-order", if it doesn't match, we immediately abort - return null; - } - } - else { - if (parentName.equals(split[splitIndex])) { - matched = true; - splitIndex--; - } - } - - parent = parent.getParentFile(); - } - } - - return null; - } - - /** - * Extracts a file from a zip into a TEMP file, if possible. The TEMP file is deleted upon JVM exit. - * - * @return the location of the extracted file, or NULL if the file cannot be extracted or doesn't exist. - */ - public static - String extractFromZip(String zipFile, String fileToExtract) throws IOException { - if (zipFile == null) { - throw new IllegalArgumentException("file cannot be null."); - } - - if (fileToExtract == null) { - throw new IllegalArgumentException("fileToExtract cannot be null."); - } - - ZipInputStream inputStrem = new ZipInputStream(new FileInputStream(zipFile)); - try { - while (true) { - ZipEntry entry = inputStrem.getNextEntry(); - if (entry == null) { - break; - } - - String name = entry.getName(); - if (entry.isDirectory()) { - continue; - } - - if (name.equals(fileToExtract)) { - File tempFile = FileUtil.tempFile(name); - tempFile.deleteOnExit(); - - FileOutputStream output = new FileOutputStream(tempFile); - try { - IO.copyStream(inputStrem, output); - } finally { - output.close(); - } - - return tempFile.getAbsolutePath(); - } - } - } finally { - inputStrem.close(); - } - - return null; - } - - /** - * Touches a file, so that it's timestamp is right now. If the file is not created, it will be created automatically. - * - * @return true if the touch succeeded, false otherwise - */ - public static - boolean touch(String file) { - long timestamp = System.currentTimeMillis(); - return touch(new File(file).getAbsoluteFile(), timestamp); - } - - /** - * Touches a file, so that it's timestamp is right now. If the file is not created, it will be created automatically. - * - * @return true if the touch succeeded, false otherwise - */ - public static - boolean touch(File file) { - long timestamp = System.currentTimeMillis(); - return touch(file, timestamp); - } - - /** - * Touches a file, so that it's timestamp is right now. If the file is not created, it will be created automatically. - * - * @return true if the touch succeeded, false otherwise - */ - public static - boolean touch(File file, long timestamp) { - if (!file.exists()) { - boolean mkdirs = file.getParentFile() - .mkdirs(); - if (!mkdirs) { - // error creating the parent directories. - return false; - } - - try { - new FileOutputStream(file).close(); - } catch (IOException ignored) { - return false; - } - } - - return file.setLastModified(timestamp); - } - - - //----------------------------------------------------------------------- - - /* - * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License - * http://commons.apache.org/proper/commons-io/ - * Copyright 2013 ASF - * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, - * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, - * Jeremias Maerki, Stephen Colebourne - */ - - /** - * Normalizes a path, removing double and single dot path steps. - *

- * THIS IS DIFFERENT in that it might not be a path that resolves to anything - *

- * This method normalizes a path to a standard format. - * The input may contain separators in either Unix or Windows format. - * The output will contain separators in the format of the system. - *

- * A trailing slash will be retained. - * A double slash will be merged to a single slash (but UNC names are handled). - * A single dot path segment will be removed. - * A double dot will cause that path segment and the one before to be removed. - * If the double dot has no parent path segment to work with, {@code null} - * is returned. - *

- * The output will be the same on both Unix and Windows except - * for the separator character. - *

-     * /foo//               -->   /foo/
-     * /foo/./              -->   /foo/
-     * /foo/../bar          -->   /bar
-     * /foo/../bar/         -->   /bar/
-     * /foo/../bar/../baz   -->   /baz
-     * //foo//./bar         -->   /foo/bar
-     * /../                 -->   null
-     * ../foo               -->   null
-     * foo/bar/..           -->   foo/
-     * foo/../../bar        -->   null
-     * foo/../bar           -->   bar
-     * //server/foo/../bar  -->   //server/bar
-     * //server/../bar      -->   null
-     * C:\foo\..\bar        -->   C:\bar
-     * C:\..\bar            -->   null
-     * ~/foo/../bar/        -->   ~/bar/
-     * ~/../bar             -->   null
-     * 
- * (Note the file separator returned will be correct for Windows/Unix) - * - * @param filename the filename to normalize, null returns null - * - * @return the normalized filename, or null if invalid - */ - public static - String normalizeRaw(String filename) { - return doNormalize(filename, SYSTEM_SEPARATOR, true); - } - - /* - * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License - * http://commons.apache.org/proper/commons-io/ - * Copyright 2013 ASF - * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, - * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, - * Jeremias Maerki, Stephen Colebourne - */ - - /** - * Normalizes a path, removing double and single dot path steps. - *

- * This method normalizes a path to a standard format. - * The input may contain separators in either Unix or Windows format. - * The output will contain separators in the format of the system. - *

- * A trailing slash will be retained. - * A double slash will be merged to a single slash (but UNC names are handled). - * A single dot path segment will be removed. - * A double dot will cause that path segment and the one before to be removed. - * If the double dot has no parent path segment to work with, {@code null} - * is returned. - *

- * The output will be the same on both Unix and Windows except - * for the separator character. - *

-     * /foo//               -->   /foo/
-     * /foo/./              -->   /foo/
-     * /foo/../bar          -->   /bar
-     * /foo/../bar/         -->   /bar/
-     * /foo/../bar/../baz   -->   /baz
-     * //foo//./bar         -->   /foo/bar
-     * /../                 -->   null
-     * ../foo               -->   null
-     * foo/bar/..           -->   foo/
-     * foo/../../bar        -->   null
-     * foo/../bar           -->   bar
-     * //server/foo/../bar  -->   //server/bar
-     * //server/../bar      -->   null
-     * C:\foo\..\bar        -->   C:\bar
-     * C:\..\bar            -->   null
-     * ~/foo/../bar/        -->   ~/bar/
-     * ~/../bar             -->   null
-     * 
- * (Note the file separator returned will be correct for Windows/Unix) - * - * @param filename the file to normalize, null returns null - * @return the normalized file, or null if invalid - */ - public static - File normalize(String filename) { - if (filename == null) { - return null; - } - - String asString = doNormalize(new File(filename).getAbsolutePath(), SYSTEM_SEPARATOR, true); - if (asString == null) { - return null; - } - - return new File(asString).getAbsoluteFile(); - } - - /* - * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License - * http://commons.apache.org/proper/commons-io/ - * Copyright 2013 ASF - * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, - * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, - * Jeremias Maerki, Stephen Colebourne - */ - - /** - * Normalizes a path, removing double and single dot path steps. - *

- * This method normalizes a path to a standard format. - * The input may contain separators in either Unix or Windows format. - * The output will contain separators in the format of the system. - *

- * A trailing slash will be retained. - * A double slash will be merged to a single slash (but UNC names are handled). - * A single dot path segment will be removed. - * A double dot will cause that path segment and the one before to be removed. - * If the double dot has no parent path segment to work with, {@code null} - * is returned. - *

- * The output will be the same on both Unix and Windows except - * for the separator character. - *

-     * /foo//               -->   /foo/
-     * /foo/./              -->   /foo/
-     * /foo/../bar          -->   /bar
-     * /foo/../bar/         -->   /bar/
-     * /foo/../bar/../baz   -->   /baz
-     * //foo//./bar         -->   /foo/bar
-     * /../                 -->   null
-     * ../foo               -->   null
-     * foo/bar/..           -->   foo/
-     * foo/../../bar        -->   null
-     * foo/../bar           -->   bar
-     * //server/foo/../bar  -->   //server/bar
-     * //server/../bar      -->   null
-     * C:\foo\..\bar        -->   C:\bar
-     * C:\..\bar            -->   null
-     * ~/foo/../bar/        -->   ~/bar/
-     * ~/../bar             -->   null
-     * 
- * (Note the file separator returned will be correct for Windows/Unix) - * - * @param file the file to normalize, null returns null - * @return the normalized file, or null if invalid - */ - public static - File normalize(File file) { - if (file == null) { - return null; - } - - String asString = doNormalize(file.getAbsolutePath(), SYSTEM_SEPARATOR, true); - if (asString == null) { - return null; - } - - return new File(asString).getAbsoluteFile(); - } - - /* - * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License - * http://commons.apache.org/proper/commons-io/ - * Copyright 2013 ASF - * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, - * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, - * Jeremias Maerki, Stephen Colebourne - */ - - /** - * Normalizes a path, removing double and single dot path steps. - *

- * This method normalizes a path to a standard format. - * The input may contain separators in either Unix or Windows format. - * The output will contain separators in the format specified. - *

- * A trailing slash will be retained. - * A double slash will be merged to a single slash (but UNC names are handled). - * A single dot path segment will be removed. - * A double dot will cause that path segment and the one before to be removed. - * If the double dot has no parent path segment to work with, {@code null} - * is returned. - *

- * The output will be the same on both Unix and Windows except - * for the separator character. - *

-     * /foo//               -->   /foo/
-     * /foo/./              -->   /foo/
-     * /foo/../bar          -->   /bar
-     * /foo/../bar/         -->   /bar/
-     * /foo/../bar/../baz   -->   /baz
-     * //foo//./bar         -->   /foo/bar
-     * /../                 -->   null
-     * ../foo               -->   null
-     * foo/bar/..           -->   foo/
-     * foo/../../bar        -->   null
-     * foo/../bar           -->   bar
-     * //server/foo/../bar  -->   //server/bar
-     * //server/../bar      -->   null
-     * C:\foo\..\bar        -->   C:\bar
-     * C:\..\bar            -->   null
-     * ~/foo/../bar/        -->   ~/bar/
-     * ~/../bar             -->   null
-     * 
- * The output will be the same on both Unix and Windows including - * the separator character. - * - * @param filename the filename to normalize, null returns null - * @param unixSeparator {@code true} if a unix separator should - * be used or {@code false} if a windows separator should be used. - * @return the normalized filename, or null if invalid - */ - public static - String normalize(String filename, boolean unixSeparator) { - char separator = unixSeparator ? UNIX_SEPARATOR : WINDOWS_SEPARATOR; - return doNormalize(filename, separator, true); - } - - //----------------------------------------------------------------------- - /* - * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License - * http://commons.apache.org/proper/commons-io/ - * Copyright 2013 ASF - * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, - * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, - * Jeremias Maerki, Stephen Colebourne - */ - - /** - * Normalizes a path, removing double and single dot path steps, - * and removing any final directory separator. - *

- * This method normalizes a path to a standard format. - * The input may contain separators in either Unix or Windows format. - * The output will contain separators in the format of the system. - *

- * A trailing slash will be removed. - * A double slash will be merged to a single slash (but UNC names are handled). - * A single dot path segment will be removed. - * A double dot will cause that path segment and the one before to be removed. - * If the double dot has no parent path segment to work with, {@code null} - * is returned. - *

- * The output will be the same on both Unix and Windows except - * for the separator character. - *

-     * /foo//               -->   /foo
-     * /foo/./              -->   /foo
-     * /foo/../bar          -->   /bar
-     * /foo/../bar/         -->   /bar
-     * /foo/../bar/../baz   -->   /baz
-     * //foo//./bar         -->   /foo/bar
-     * /../                 -->   null
-     * ../foo               -->   null
-     * foo/bar/..           -->   foo
-     * foo/../../bar        -->   null
-     * foo/../bar           -->   bar
-     * //server/foo/../bar  -->   //server/bar
-     * //server/../bar      -->   null
-     * C:\foo\..\bar        -->   C:\bar
-     * C:\..\bar            -->   null
-     * ~/foo/../bar/        -->   ~/bar
-     * ~/../bar             -->   null
-     * 
- * (Note the file separator returned will be correct for Windows/Unix) - * - * @param filename the filename to normalize, null returns null - * @return the normalized filename, or null if invalid - */ - public static - String normalizeNoEndSeparator(String filename) { - return doNormalize(filename, SYSTEM_SEPARATOR, false); - } - - /* - * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License - * http://commons.apache.org/proper/commons-io/ - * Copyright 2013 ASF - * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, - * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, - * Jeremias Maerki, Stephen Colebourne - */ - - /** - * Normalizes a path, removing double and single dot path steps, - * and removing any final directory separator. - *

- * This method normalizes a path to a standard format. - * The input may contain separators in either Unix or Windows format. - * The output will contain separators in the format specified. - *

- * A trailing slash will be removed. - * A double slash will be merged to a single slash (but UNC names are handled). - * A single dot path segment will be removed. - * A double dot will cause that path segment and the one before to be removed. - * If the double dot has no parent path segment to work with, {@code null} - * is returned. - *

- * The output will be the same on both Unix and Windows including - * the separator character. - *

-     * /foo//               -->   /foo
-     * /foo/./              -->   /foo
-     * /foo/../bar          -->   /bar
-     * /foo/../bar/         -->   /bar
-     * /foo/../bar/../baz   -->   /baz
-     * //foo//./bar         -->   /foo/bar
-     * /../                 -->   null
-     * ../foo               -->   null
-     * foo/bar/..           -->   foo
-     * foo/../../bar        -->   null
-     * foo/../bar           -->   bar
-     * //server/foo/../bar  -->   //server/bar
-     * //server/../bar      -->   null
-     * C:\foo\..\bar        -->   C:\bar
-     * C:\..\bar            -->   null
-     * ~/foo/../bar/        -->   ~/bar
-     * ~/../bar             -->   null
-     * 
- * - * @param filename the filename to normalize, null returns null - * @param unixSeparator {@code true} if a unix separator should - * be used or {@code false} if a windows separtor should be used. - * @return the normalized filename, or null if invalid - */ - public static - String normalizeNoEndSeparator(String filename, boolean unixSeparator) { - char separator = unixSeparator ? UNIX_SEPARATOR : WINDOWS_SEPARATOR; - return doNormalize(filename, separator, false); - } - - /* - * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License - * http://commons.apache.org/proper/commons-io/ - * Copyright 2013 ASF - * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, - * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, - * Jeremias Maerki, Stephen Colebourne - */ - - /** - * Internal method to perform the normalization. - * - * @param filename the filename - * @param separator The separator character to use - * @param keepSeparator true to keep the final separator - * @return the normalized filename - */ - private static - String doNormalize(String filename, char separator, boolean keepSeparator) { - if (filename == null) { - return null; - } - int size = filename.length(); - if (size == 0) { - return filename; - } - - int prefix = getPrefixLength(filename); - if (prefix < 0) { - return null; - } - - char[] array = new char[size + 2]; // +1 for possible extra slash, +2 for arraycopy - filename.getChars(0, filename.length(), array, 0); - - // fix separators throughout - char otherSeparator = separator == SYSTEM_SEPARATOR ? OTHER_SEPARATOR : SYSTEM_SEPARATOR; - for (int i = 0; i < array.length; i++) { - if (array[i] == otherSeparator) { - array[i] = separator; - } - } - - // add extra separator on the end to simplify code below - boolean lastIsDirectory = true; - if (array[size - 1] != separator) { - array[size++] = separator; - lastIsDirectory = false; - } - - // adjoining slashes - for (int i = prefix + 1; i < size; i++) { - if (array[i] == separator && array[i - 1] == separator) { - System.arraycopy(array, i, array, i - 1, size - i); - size--; - i--; - } - } - - // dot slash - for (int i = prefix + 1; i < size; i++) { - if (array[i] == separator && array[i - 1] == '.' && - (i == prefix + 1 || array[i - 2] == separator)) { - if (i == size - 1) { - lastIsDirectory = true; - } - System.arraycopy(array, i + 1, array, i - 1, size - i); - size -= 2; - i--; - } - } - - // double dot slash - outer: - for (int i = prefix + 2; i < size; i++) { - if (array[i] == separator && array[i - 1] == '.' && array[i - 2] == '.' && - (i == prefix + 2 || array[i - 3] == separator)) { - if (i == prefix + 2) { - return null; - } - if (i == size - 1) { - lastIsDirectory = true; - } - int j; - for (j = i - 4; j >= prefix; j--) { - if (array[j] == separator) { - // remove b/../ from a/b/../c - System.arraycopy(array, i + 1, array, j + 1, size - i); - size -= i - j; - i = j + 1; - continue outer; - } - } - // remove a/../ from a/../c - System.arraycopy(array, i + 1, array, prefix, size - i); - size -= i + 1 - prefix; - i = prefix + 1; - } - } - - if (size <= 0) { // should never be less than 0 - return ""; - } - if (size <= prefix) { // should never be less than prefix - return new String(array, 0, size); - } - if (lastIsDirectory && keepSeparator) { - return new String(array, 0, size); // keep trailing separator - } - return new String(array, 0, size - 1); // lose trailing separator - } - - //----------------------------------------------------------------------- - /* - * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License - * http://commons.apache.org/proper/commons-io/ - * Copyright 2013 ASF - * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, - * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, - * Jeremias Maerki, Stephen Colebourne - */ - - /** - * Returns the length of the filename prefix, such as C:/ or ~/. - *

- * This method will handle a file in either Unix or Windows format. - *

- * The prefix length includes the first slash in the full filename - * if applicable. Thus, it is possible that the length returned is greater - * than the length of the input string. - *

-     * Windows:
-     * a\b\c.txt           --> ""          --> relative
-     * \a\b\c.txt          --> "\"         --> current drive absolute
-     * C:a\b\c.txt         --> "C:"        --> drive relative
-     * C:\a\b\c.txt        --> "C:\"       --> absolute
-     * \\server\a\b\c.txt  --> "\\server\" --> UNC
-     *
-     * Unix:
-     * a/b/c.txt           --> ""          --> relative
-     * /a/b/c.txt          --> "/"         --> absolute
-     * ~/a/b/c.txt         --> "~/"        --> current user
-     * ~                   --> "~/"        --> current user (slash added)
-     * ~user/a/b/c.txt     --> "~user/"    --> named user
-     * ~user               --> "~user/"    --> named user (slash added)
-     * 
- *

- * The output will be the same irrespective of the machine that the code is running on. - * ie. both Unix and Windows prefixes are matched regardless. - * - * @param filename the filename to find the prefix in, null returns -1 - * @return the length of the prefix, -1 if invalid or null - */ - public static - int getPrefixLength(String filename) { - if (filename == null) { - return -1; - } - int len = filename.length(); - if (len == 0) { - return 0; - } - char ch0 = filename.charAt(0); - if (ch0 == ':') { - return -1; - } - if (len == 1) { - if (ch0 == '~') { - return 2; // return a length greater than the input - } - return isSeparator(ch0) ? 1 : 0; - } - else { - if (ch0 == '~') { - int posUnix = filename.indexOf(UNIX_SEPARATOR, 1); - int posWin = filename.indexOf(WINDOWS_SEPARATOR, 1); - if (posUnix == -1 && posWin == -1) { - return len + 1; // return a length greater than the input - } - posUnix = posUnix == -1 ? posWin : posUnix; - posWin = posWin == -1 ? posUnix : posWin; - return Math.min(posUnix, posWin) + 1; - } - char ch1 = filename.charAt(1); - if (ch1 == ':') { - ch0 = Character.toUpperCase(ch0); - if (ch0 >= 'A' && ch0 <= 'Z') { - //noinspection PointlessBooleanExpression - if (len == 2 || isSeparator(filename.charAt(2)) == false) { - return 2; - } - return 3; - } - return -1; - } - else if (isSeparator(ch0) && isSeparator(ch1)) { - int posUnix = filename.indexOf(UNIX_SEPARATOR, 2); - int posWin = filename.indexOf(WINDOWS_SEPARATOR, 2); - if (posUnix == -1 && posWin == -1 || posUnix == 2 || posWin == 2) { - return -1; - } - posUnix = posUnix == -1 ? posWin : posUnix; - posWin = posWin == -1 ? posUnix : posWin; - return Math.min(posUnix, posWin) + 1; - } - else { - return isSeparator(ch0) ? 1 : 0; - } - } - } - - //----------------------------------------------------------------------- - /* - * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License - * http://commons.apache.org/proper/commons-io/ - * Copyright 2013 ASF - * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, - * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, - * Jeremias Maerki, Stephen Colebourne - */ - - /** - * Checks if the character is a separator. - * - * @param ch the character to check - * @return true if it is a separator character - */ - private static - boolean isSeparator(char ch) { - return ch == UNIX_SEPARATOR || ch == WINDOWS_SEPARATOR; - } - - - /** - * Gets the extension of a file (text after the last '.') - * - * @return "" if there is no extension or fileName is null - */ - public static - String getExtension(String fileName) { - if (fileName == null) { - return ""; - } - - int dot = fileName.lastIndexOf('.'); - if (dot > -1) { - return fileName.substring(dot + 1); - } - else { - return ""; - } - } - - /** - * Gets the name of a file that is before the extension (text before the last '.') - * - * @return non-null - */ - public static - String getNameWithoutExtension(final String fileName) { - int dot = fileName.lastIndexOf('.'); - if (dot > -1) { - return fileName.substring(0, dot); - } - else { - return fileName; - } - } -} diff --git a/src/dorkbox/util/FileUtil.kt b/src/dorkbox/util/FileUtil.kt new file mode 100644 index 0000000..6f6608e --- /dev/null +++ b/src/dorkbox/util/FileUtil.kt @@ -0,0 +1,2058 @@ +/* + * Copyright 2010 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.util + +import dorkbox.os.OS +import java.io.BufferedInputStream +import java.io.BufferedReader +import java.io.BufferedWriter +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.FileReader +import java.io.FileWriter +import java.io.IOException +import java.io.InputStream +import java.io.PrintWriter +import java.io.RandomAccessFile +import java.io.Reader +import java.nio.channels.FileChannel +import java.nio.charset.StandardCharsets +import java.nio.file.DirectoryIteratorException +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes +import java.util.* +import java.util.zip.* + +/** + * File related utilities. + * + * + * Contains code from FilenameUtils.java (normalize + dependencies) - Apache 2.0 License + * http://commons.apache.org/proper/commons-io/ + * Copyright 2013 ASF + * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, + * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, + * Jeremias Maerki, Stephen Colebourne + */ +@Suppress("unused") +object FileUtil { + interface Action { + fun onLineRead(line: String) + fun finished() + } + + private val log = logger() + + private const val DEBUG = false + + /** + * The Unix separator character. + */ + private const val UNIX_SEPARATOR = '/' + + /** + * The Windows separator character. + */ + private const val WINDOWS_SEPARATOR = '\\' + + /** + * The system separator character. + */ + private val SYSTEM_SEPARATOR = File.separatorChar + + /** + * The separator character that is the opposite of the system separator. + */ + private val OTHER_SEPARATOR = if (OS.isWindows) { UNIX_SEPARATOR } else { WINDOWS_SEPARATOR } + + var ZIP_HEADER = byteArrayOf('P'.code.toByte(), 'K'.code.toByte(), 0x3.toByte(), 0x4.toByte()) + + fun prepend(file: File, vararg strings: String) { + // make sure we can write to the file + file.parentFile?.mkdirs() + + val contents = LinkedList() + + for (string in strings) { + contents.add(string) + } + + // have to read ORIGINAL file, since we can't prepend it any other way.... + readOnePerLine(file, contents, false) + + write(file, contents) + } + + fun append(file: File, vararg text: String) { + // make sure we can write to the file + file.parentFile?.mkdirs() + + // wooooo for auto-closable and try-with-resources + try { + FileWriter(file, true).use { fw -> + BufferedWriter(fw).use { bw -> + PrintWriter(bw).use { out -> + + for (s in text) { + out.println(s) + } + } + } + } + } + catch (e: IOException) { + log.error("Error appending text", e) + } + } + + fun write(file: File, vararg text: String) { + // make sure we can write to the file + file.parentFile?.mkdirs() + + // wooooo for auto-closable and try-with-resources + try { + FileWriter(file, false).use { fw -> + BufferedWriter(fw).use { bw -> + PrintWriter(bw).use { out -> + + for (s in text) { + out.println(s) + } + } + } + } + } + catch (e: IOException) { + log.error("Error appending text", e) + } + + } + + fun write(file: File, text: List) { + // make sure we can write to the file + file.parentFile?.mkdirs() + + // wooooo for auto-closable and try-with-resources + try { + FileWriter(file, false).use { fw -> + BufferedWriter(fw).use { bw -> + PrintWriter(bw).use { out -> + + for (s in text) { + out.println(s) + } + } + } + } + } + catch (e: IOException) { + log.error("Error appending text", e) + } + + } + + fun append(file: File, text: List) { + // make sure we can write to the file + file.parentFile?.mkdirs() + + // wooooo for auto-closable and try-with-resources + try { + FileWriter(file, true).use { fw -> + BufferedWriter(fw).use { bw -> + PrintWriter(bw).use { out -> + + for (s in text) { + out.println(s) + } + } + } + } + } + catch (e: IOException) { + log.error("Error appending text", e) + } + } + + /** + * Converts the content of a file into a list of strings. Lines are trimmed. + * + * @param file the input file to read. Throws an error if this file cannot be read. + * @param includeEmptyLines true if you want the resulting list of String to include blank/empty lines from the file + * + * @return A list of strings, one line per string, of the content + */ + @Throws(IOException::class) + fun read(file: File, includeEmptyLines: Boolean): List { + val lines: MutableList = ArrayList() + FileReader(file).use { + val bin = BufferedReader(it) + var line: String + + if (includeEmptyLines) { + while (bin.readLine().also { line = it } != null) { + lines.add(line) + } + } else { + while (bin.readLine().also { line = it } != null) { + if (line.isNotEmpty()) { + lines.add(line) + } + } + } + } + + return lines + } + + /** + * Convenience method that converts the content of a file into a giant string. + * + * @param file the input file to read. Throws an error if this file cannot be read. + * + * @return A string, matching the contents of the file + */ + @Throws(IOException::class) + fun readAsString(file: File): String { + val stringBuilder = StringBuilder(file.length().toInt()) + read(file, stringBuilder) + return stringBuilder.toString() + } + + /** + * @return contents of the file if we could read the file without errors. Null if we could not + */ + fun read(file: String): String? { + return read(File(file)) + } + + /** + * @return contents of the file if we could read the file without errors. Null if we could not + */ + fun read(file: File): String? { + val stringBuilder = StringBuilder() + return if (read(file, stringBuilder, OS.LINE_SEPARATOR)) { + stringBuilder.toString() + } + else { + null + } + } + + /** + * Reads the content of a file to the passed in StringBuilder. + * + * @param file the input file to read. Throws an error if this file cannot be read. + * @param stringBuilder the stringBuilder this file will be written to + */ + @Throws(IOException::class) + fun read(file: File, stringBuilder: StringBuilder) { + FileReader(file).use { + val bin = BufferedReader(it) + var line: String? + while (bin.readLine().also { line = it } != null) { + stringBuilder.append(line).append(OS.LINE_SEPARATOR) + } + } + } + + /** + * Reads the content of a file to the passed in StringBuilder. + * + * @return true if we could read the file without errors. False if there were errors. + */ + fun read(file: File, builder: StringBuilder, lineSeparator: String?): Boolean { + if (!file.canRead()) { + return false + } + + val path = Path.of(file.toURI()) + val bufferedReader = Files.newBufferedReader(path, StandardCharsets.UTF_8) + + try { + bufferedReader.use { reader -> + while (true) { + /* + * returns the content of a line MINUS the newline. + * returns null only for the END of the stream. + * returns an empty String if two newlines appear in a row. + */ + val line = reader.readLine() ?: break + + if (lineSeparator != null) { + builder.append(line).append(lineSeparator) + } + else { + builder.append(line) + } + } + } + } + catch (ignored: Exception) { + return false + } + + return true + } + + /** + * Reads each line in a file, performing ACTION for each line. + * + * @return true if we could read the file without errors. False if there were errors. + */ + fun read(file: File, action: Action): Boolean { + if (!file.canRead()) { + return false + } + + val path = Path.of(file.toURI()) + val bufferedReader = Files.newBufferedReader(path, StandardCharsets.UTF_8) + + try { + bufferedReader.use { reader -> + while (true) { + /* + * returns the content of a line MINUS the newline. + * returns null only for the END of the stream. + * returns an empty String if two newlines appear in a row. + */ + val line = reader.readLine() ?: break + action.onLineRead(line) + } + } + } + catch (ignored: Exception) { + return false + } + action.finished() + + return true + } + + /** + * Will always return a String. + * + * @param file the file to read + * + * @return the first line in the file, excluding the "new line" character. + */ + fun readFirstLine(file: File): String { + if (!file.canRead()) { + return "" + } + + val path = Path.of(file.toURI()) + val bufferedReader = Files.newBufferedReader(path, StandardCharsets.UTF_8) + + bufferedReader.use { reader -> + while (true) { + /* + * returns the content of a line MINUS the newline. + * returns null only for the END of the stream. + * returns an empty String if two newlines appear in a row. + */ + return reader.readLine() ?: break + } + } + + return "" + } + + fun getPid(pidFileName: String): String? { + val stringBuilder = StringBuilder() + return if (read(File(pidFileName), stringBuilder, null)) { + stringBuilder.toString() + } + else { + null + } + } + + /** + * Reads the contents of the supplied input stream into a list of lines. + * + * @return Always returns a list, even if the file does not exist, or there are errors reading it. + */ + fun readLines(file: File): List { + val fileReader = try { + FileReader(file) + } catch (ignored: FileNotFoundException) { + return ArrayList() + } + return readLines(fileReader) + } + + /** + * Reads the contents of the supplied input stream into a list of lines. + * + * + * Closes the reader on successful or failed completion. + * + * @return Always returns a list, even if the file does not exist, or there are errors reading it. + */ + fun readLines(`in`: Reader): List { + val lines: MutableList = ArrayList() + + BufferedReader(`in`).use { + val bin = BufferedReader(`in`) + var line: String + try { + while (bin.readLine().also { line = it } != null) { + lines.add(line) + } + } catch (ignored: IOException) { + } + } + + return lines + } + + /** + * @return a list of the contents of a file, one line at a time. Ignores lines that start with # + */ + fun readOnePerLine(file: File): ArrayList { + val list = ArrayList() + readOnePerLine(file, list, true) + + return list + } + + /** + * @return a list of the contents of a file, one line at a time. Ignores lines that start with # + */ + fun readOnePerLine(file: File, trimStrings: Boolean): ArrayList { + val list = ArrayList() + readOnePerLine(file, list, trimStrings) + + return list + } + + /** + * @return a list of the contents of a file, one line at a time. Ignores lines that start with # + */ + fun readOnePerLine(file: File, list: MutableList, trimStrings: Boolean) { + if (trimStrings) { + read(file, object : Action { + var lineNumber = 0 + + override fun onLineRead(line: String) { + if (!line.isEmpty() && !line.startsWith("#")) { + val newLine = line.trim() + + if (!newLine.isEmpty()) { + list.add(newLine) + } + } + + lineNumber++ + } + + override fun finished() {} + }) + } + else { + read(file, object : Action { + var lineNumber = 0 + + override fun onLineRead(line: String) { + list.add(line) + lineNumber++ + } + + override fun finished() {} + }) + } + } + + fun deleteDirectory(dir: File) { + try { + val directory = Path.of(dir.absolutePath) + Files.walkFileTree(directory, object : SimpleFileVisitor() { + @kotlin.jvm.Throws(IOException::class) + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + Files.delete(file) + return FileVisitResult.CONTINUE + } + + @kotlin.jvm.Throws(IOException::class) + override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult { + Files.delete(dir) + return FileVisitResult.CONTINUE + } + }) + } + catch (e: IOException) { + log.error("Error deleting the contents of dir $dir", e) + } + catch (e: DirectoryIteratorException) { + log.error("Error deleting the contents of dir $dir", e) + } + + } + + /** + * Renames a file. Windows has all sorts of problems which are worked around. + * + * @return true if successful, false otherwise + */ + fun renameTo(source: File, dest: File): Boolean { + // if we're on a civilized operating system we may be able to simple + // rename it + if (source.renameTo(dest)) { + return true + } + + // fall back to trying to rename the old file out of the way, rename the + // new file into + // place and then delete the old file + if (dest.exists()) { + val temp = File(dest.path + "_old") + if (temp.exists()) { + if (!temp.delete()) { + if (DEBUG) { + System.err.println("Failed to delete old intermediate file: $temp") + } + + // the subsequent code will probably fail + } + } + if (dest.renameTo(temp)) { + if (source.renameTo(dest)) { + if (temp.delete()) { + if (DEBUG) { + System.err.println("Failed to delete intermediate file: $temp") + } + } + return true + } + } + } + + // as a last resort, try copying the old data over the new + return try { + FileInputStream(source).use { fin -> + FileOutputStream(dest).use { fout -> + fin.copyTo(fout) + + if (!source.delete()) { + if (DEBUG) { + System.err.println("Failed to delete '$source' after brute force copy to '$dest'.") + } + } + true + } + } + } catch (ioe: IOException) { + if (DEBUG) { + System.err.println("Failed to copy '$source' to '$dest'.") + ioe.printStackTrace() + } + false + } + } + + /** + * Copies a files from one location to another. Overwriting any existing file at the destination. + */ + @Throws(IOException::class) + fun copyFile(`in`: String, out: File): File { + return copyFile(File(`in`), out) + } + + /** + * Copies a files from one location to another. Overwriting any existing file at the destination. + */ + @Throws(IOException::class) + fun copyFile(`in`: File, out: String): File { + return copyFile(`in`, File(out)) + } + + /** + * Copies a files from one location to another. Overwriting any existing file at the destination. + */ + @Throws(IOException::class) + fun copyFile(`in`: String, out: String): File { + return copyFile(File(`in`), File(out)) + } + + /** + * Copies a files from one location to another. Overwriting any existing file at the destination. + */ + @Throws(IOException::class) + fun copyFileToDir(`in`: String, out: String): File { + return copyFileToDir(File(`in`), File(out)) + } + + /** + * Copies a files from one location to another. Overwriting any existing file at the destination. + * If the out file is a directory, then the in file will be copied to the directory + */ + @Throws(IOException::class) + fun copyFileToDir(`in`: File, out: File): File { + // copy the file to the directory instead + if (!out.isDirectory) { + throw IOException("Out file is not a directory! '" + out.absolutePath + "'") + } + return copyFile(`in`, File(out, `in`.name)) + } + + /** + * Copies a files from one location to another. Overwriting any existing file at the destination. + */ + @JvmStatic + @Throws(IOException::class) + fun copyFile(`in`: File, out: File): File { + val normalizedIn = normalize(`in`)!!.absolutePath + val normalizedout = normalize(out)!!.absolutePath + if (normalizedIn.equals(normalizedout, ignoreCase = true)) { + if (DEBUG) { + System.err.println("Source equals destination! $normalizedIn") + } + return out + } + + + // if out doesn't exist, then create it. + val parentOut: File? = out.parentFile + if (parentOut?.canWrite() == false) { + parentOut.mkdirs() + } + if (DEBUG) { + System.err.println("Copying file: '$`in`' --> '$out'") + } + var sourceChannel: FileChannel? = null + var destinationChannel: FileChannel? = null + try { + sourceChannel = FileInputStream(normalizedIn).channel + destinationChannel = FileOutputStream(normalizedout).channel + if (sourceChannel.size() == 0L) { + System.err.println("Source size is ZERO: $normalizedIn") + } + + sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel) + } finally { + try { + sourceChannel?.close() + } catch (ignored: Exception) { + } + try { + destinationChannel?.close() + } catch (ignored: Exception) { + } + } + out.setLastModified(`in`.lastModified()) + return out + } + + /** + * Copies the contents of file two onto the END of file one. + */ + fun concatFiles(one: File, two: File): File { + val normalizedOne = normalize(one)!!.absolutePath + val normalizedTwo = normalize(two)!!.absolutePath + if (DEBUG) { + System.err.println("Concat'ing file: '$one' --> '$two'") + } + var channelOne: FileChannel? = null + var channelTwo: FileChannel? = null + try { + // open it in append mode + channelOne = FileOutputStream(normalizedOne, true).channel + channelTwo = FileInputStream(normalizedTwo).channel + var size = two.length() + while (size > 0) { + size -= channelOne.transferFrom(channelTwo, 0, size) + } + } catch (ignored: Exception) { + ignored.printStackTrace() + } finally { + try { + channelOne?.close() + } catch (ignored: Exception) { + } + try { + channelTwo?.close() + } catch (ignored: Exception) { + } + } + one.setLastModified(System.currentTimeMillis()) + return one + } + + /** + * Moves a file, overwriting any existing file at the destination. + */ + @Throws(IOException::class) + fun moveFile(`in`: String, out: File): File { + return moveFile(File(`in`), out) + } + + /** + * Moves a file, overwriting any existing file at the destination. + */ + @Throws(IOException::class) + fun moveFile(`in`: File, out: String): File { + return moveFile(`in`, File(out)) + } + + /** + * Moves a file, overwriting any existing file at the destination. + */ + @Throws(IOException::class) + fun moveFile(`in`: String, out: String): File { + return moveFile(File(`in`), File(out)) + } + + /** + * Moves a file, overwriting any existing file at the destination. + */ + @Throws(IOException::class) + fun moveFile(`in`: File, out: File): File { + if (out.canRead()) { + out.delete() + } + val renameSuccess = renameTo(`in`, out) + if (!renameSuccess) { + throw IOException("Unable to move file: '" + `in`.absolutePath + "' -> '" + out.absolutePath + "'") + } + return out + } + + /** + * Copies a directory from one location to another + */ + @Throws(IOException::class) + fun copyDirectory(src: String, dest: String, vararg namesToIgnore: String) { + copyDirectory(File(src), File(dest), *namesToIgnore) + } + + /** + * Copies a directory from one location to another + */ + @Throws(IOException::class) + fun copyDirectory(src_: File, dest_: File, vararg namesToIgnore: String) { + val src = normalize(src_) + val dest = normalize(dest_) + + requireNotNull(src) { "Source must be valid" } + requireNotNull(dest) { "Destination must be valid" } + + if (namesToIgnore.size > 0) { + val name = src.name + for (ignore in namesToIgnore) { + if (name == ignore) { + return + } + } + } + if (src.isDirectory) { + // if directory not exists, create it + if (!dest.exists()) { + dest.mkdir() + if (DEBUG) { + System.err.println("Directory copied from '$src' --> '$dest'") + } + } + + // list all the directory contents + val files = src.list() + if (files != null) { + for (file in files) { + // construct the src and dest file structure + val srcFile = File(src, file) + val destFile = File(dest, file) + + // recursive copy + copyDirectory(srcFile, destFile, *namesToIgnore) + } + } + } else { + // if file, then copy it + copyFile(src, dest) + } + } + + /** + * Safely moves a directory from one location to another (by copying it first, then deleting the original). + */ + @Throws(IOException::class) + fun moveDirectory(src: String, dest: String, vararg fileNamesToIgnore: String) { + moveDirectory(File(src), File(dest), *fileNamesToIgnore) + } + + /** + * Safely moves a directory from one location to another (by copying it first, then deleting the original). + */ + @Throws(IOException::class) + fun moveDirectory(src: File, dest: File, vararg fileNamesToIgnore: String) { + if (fileNamesToIgnore.size > 0) { + val name = src.name + for (ignore in fileNamesToIgnore) { + if (name == ignore) { + return + } + } + } + if (src.isDirectory) { + // if directory not exists, create it + if (!dest.exists()) { + dest.mkdir() + if (DEBUG) { + System.err.println("Directory copied from '$src' --> '$dest'") + } + } + + // list all the directory contents + val files = src.list() + if (files != null) { + for (file in files) { + // construct the src and dest file structure + val srcFile = File(src, file) + val destFile = File(dest, file) + + // recursive copy + moveDirectory(srcFile, destFile, *fileNamesToIgnore) + } + } + } else { + // if file, then copy it + moveFile(src, dest) + } + } + + /** + * Deletes a file or directory and all files and sub-directories under it. + * + * @param fileNamesToIgnore if prefaced with a '/', it will ignore as a directory instead of file + * @return true iff the file/dir was deleted + */ + fun delete(fileName: String, vararg fileNamesToIgnore: String): Boolean { + return delete(File(fileName), *fileNamesToIgnore) + } + + /** + * Deletes a file, directory + all files and sub-directories under it. The directory is ALSO deleted if it because empty as a result + * of this operation + * + * @param namesToIgnore if prefaced with a '/', it will treat the name to ignore as a directory instead of file + * + * @return true IFF the file/dir was deleted or didn't exist at first + */ + @JvmStatic + fun delete(file: File, vararg namesToIgnore: String): Boolean { + if (!file.exists()) { + return true + } + var thingsDeleted = false + var ignored = false + if (file.isDirectory) { + val files = file.listFiles() + if (files != null) { + var i = 0 + val n = files.size + while (i < n) { + var delete = true + val file2 = files[i] + val name2 = file2.name + val name2Full = normalize(file2)!!.absolutePath + if (file2.isDirectory) { + for (name in namesToIgnore) { + if (name[0] == UNIX_SEPARATOR && name == name2) { + // only name match if our name To Ignore starts with a / or \ + if (DEBUG) { + System.err.println("Skipping delete dir: $file2") + } + ignored = true + delete = false + break + } else if (name == name2Full) { + // full path match + if (DEBUG) { + System.err.println("Skipping delete dir: $file2") + } + ignored = true + delete = false + break + } + } + if (delete) { + if (DEBUG) { + System.err.println("Deleting dir: $file2") + } + delete(file2, *namesToIgnore) + } + } else { + for (name in namesToIgnore) { + if (name[0] != UNIX_SEPARATOR && name == name2) { + // only name match + if (DEBUG) { + System.err.println("Skipping delete file: $file2") + } + ignored = true + delete = false + break + } else if (name == name2Full) { + // full path match + if (DEBUG) { + System.err.println("Skipping delete file: $file2") + } + ignored = true + delete = false + break + } + } + if (delete) { + if (DEBUG) { + System.err.println("Deleting file: $file2") + } + thingsDeleted = thingsDeleted or file2.delete() + } + } + i++ + } + } + } + + // don't try to delete the dir if there was an ignored file in it + if (ignored) { + if (DEBUG) { + System.err.println("Skipping deleting file: $file") + } + return false + } + if (DEBUG) { + System.err.println("Deleting file: $file") + } + thingsDeleted = thingsDeleted or file.delete() + return thingsDeleted + } + + /** + * @return the contents of the file as a byte array + */ + fun toBytes(file: File): ByteArray? { + FileInputStream(file.absolutePath).use { + return it.readAllBytes() + } + } + + /** + * Creates the directories in the specified location. + */ + fun mkdir(location: File): String { + val path = normalize(location)!!.absolutePath + if (location.mkdirs()) { + if (DEBUG) { + System.err.println("Created directory: $path") + } + } + return path + } + + /** + * Creates the directories in the specified location. + */ + fun mkdir(location: String): String { + return mkdir(File(location)) + } + + /** + * Creates a temp file + */ + @Throws(IOException::class) + fun tempFile(fileName: String): File { + return normalize(File.createTempFile(fileName, null))!!.absoluteFile + } + + /** + * Creates a temp directory + */ + @Throws(IOException::class) + fun tempDirectory(directoryName: String): String { + val file = File.createTempFile(directoryName, null) + if (!file.delete()) { + throw IOException("Unable to delete temp file: $file") + } + if (!file.mkdir()) { + throw IOException("Unable to create temp directory: $file") + } + return normalize(file)!!.absolutePath + } + + /** + * @return true if the inputStream is a zip/jar stream. DOES NOT CLOSE THE STREAM + */ + fun isZipStream(`in`: InputStream): Boolean { + var `in` = `in` + if (!`in`.markSupported()) { + `in` = BufferedInputStream(`in`) + } + var isZip = true + try { + `in`.mark(ZIP_HEADER.size) + for (i in ZIP_HEADER.indices) { + if (ZIP_HEADER[i] != `in`.read().toByte()) { + isZip = false + break + } + } + `in`.reset() + } catch (e: Exception) { + isZip = false + } + return isZip + } + + /** + * @return true if the named file is a zip/jar file + */ + fun isZipFile(fileName: String): Boolean { + return isZipFile(File(fileName)) + } + + /** + * @return true if the file is a zip/jar file + */ + fun isZipFile(file: File): Boolean { + var isZip = true + val buffer = ByteArray(ZIP_HEADER.size) + var raf: RandomAccessFile? = null + try { + raf = RandomAccessFile(file, "r") + raf.readFully(buffer) + for (i in ZIP_HEADER.indices) { + if (buffer[i] != ZIP_HEADER[i]) { + isZip = false + break + } + } + } catch (e: Exception) { + isZip = false + (e as? FileNotFoundException)?.printStackTrace() + } finally { + if (raf != null) { + try { + raf.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + } + return isZip + } + + /** + * Unzips a ZIP file + */ + @Throws(IOException::class) + fun unzip(zipFile: String, outputDir: String) { + unzipJar(zipFile, outputDir, true) + } + + /** + * Unzips a ZIP file + */ + @Throws(IOException::class) + fun unzip(zipFile: File, outputDir: File) { + unzipJar(zipFile, outputDir, true) + } + + /** + * Unzips a ZIP file. Will close the input stream. + */ + @Throws(IOException::class) + fun unzip(inputStream: ZipInputStream, outputDir: String) { + unzip(inputStream, File(outputDir)) + } + + /** + * Unzips a ZIP file. Will close the input stream. + */ + @Throws(IOException::class) + fun unzip(inputStream: ZipInputStream, outputDir: File) { + unzipJar(inputStream, outputDir, true) + } + + /** + * Unzips a ZIP file + */ + @Throws(IOException::class) + fun unzipJar(zipFile: String, outputDir: String, extractManifest: Boolean) { + unjarzip0(File(zipFile), File(outputDir), extractManifest) + } + + /** + * Unzips a ZIP file + */ + @Throws(IOException::class) + fun unzipJar(zipFile: File, outputDir: File, extractManifest: Boolean) { + unjarzip0(zipFile, outputDir, extractManifest) + } + + /** + * Unzips a ZIP file. Will close the input stream. + */ + @Throws(IOException::class) + fun unzipJar(inputStream: ZipInputStream, outputDir: File, extractManifest: Boolean) { + unjarzip1(inputStream, outputDir, extractManifest) + } + + /** + * Unzips a ZIP or JAR file (and handles the manifest if requested) + */ + @Throws(IOException::class) + private fun unjarzip0(zipFile: File, outputDir: File, extractManifest: Boolean) { + val fileLength = zipFile.length() + if (fileLength > Int.MAX_VALUE - 1) { + throw RuntimeException("Source filesize is too large!") + } + val inputStream = ZipInputStream(FileInputStream(zipFile)) + unjarzip1(inputStream, outputDir, extractManifest) + } + + /** + * Unzips a ZIP file + */ + @Throws(IOException::class) + private fun unjarzip1(inputStream: ZipInputStream, outputDir: File, extractManifest: Boolean) { + + inputStream.use { + var entry: ZipEntry + while (inputStream.nextEntry.also { entry = it } != null) { + val name = entry.name + if (!extractManifest && name.startsWith("META-INF/")) { + continue + } + val file = File(outputDir, name) + if (entry.isDirectory) { + mkdir(file.path) + continue + } + mkdir(file.parent) + + FileOutputStream(file).use { + inputStream.copyTo(it) + } + } + } + } + + /** + * Parses the specified root directory for **ALL** files that are in it. All of the sub-directories are searched as well. + * + * + * *This is different, in that it returns ALL FILES, instead of ones that just match a specific extension.* + * + * @return the list of all files in the root+sub-dirs. + */ + @Throws(IOException::class) + fun parseDir(rootDirectory: String): List { + return parseDir(File(rootDirectory)) + } + + /** + * Parses the specified root directory for **ALL** files that are in it. All of the sub-directories are searched as well. + * + * + * *This is different, in that it returns ALL FILES, instead of ones that just match a specific extension.* + * + * @return the list of all files in the root+sub-dirs. + */ + @Throws(IOException::class) + fun parseDir(rootDirectory: File): List { + return parseDir(rootDirectory) + } + + /** + * Parses the specified root directory for files that end in the extension to match. All of the sub-directories are searched as well. + * + * @return the list of all files in the root+sub-dirs that match the given extension. + */ + @Throws(IOException::class) + fun parseDir(rootDirectory: File, vararg extensionsToMatch: String): List { + val jarList: MutableList = LinkedList() + val directories = LinkedList() + + val rootDirectory = normalize(rootDirectory) ?: throw IOException("Root directory was invalid!") + + if (!rootDirectory.exists()) { + throw IOException("Location does not exist: " + rootDirectory.absolutePath) + } + + if (rootDirectory.isDirectory) { + directories.add(rootDirectory) + while (directories.peek() != null) { + val dir = directories.poll() + val listFiles = dir!!.listFiles() + if (listFiles != null) { + for (file in listFiles) { + if (file.isDirectory) { + directories.add(file) + } else { + if (extensionsToMatch.isEmpty()) { + jarList.add(file) + } else { + for (e in extensionsToMatch) { + if (file.absolutePath.endsWith(e)) { + jarList.add(file) + } + } + } + } + } + } + } + } else { + throw IOException("Cannot search directory children if the dir is a file name: " + rootDirectory.absolutePath) + } + return jarList + } + + /** + * Gets the relative path of a file to a specific directory in it's hierarchy. + * + * + * For example: getChildRelativeToDir("/a/b/c/d/e.bah", "c") -> "d/e.bah" + * + * @return null if there is no child + */ + fun getChildRelativeToDir(fileName: String, dirInHeirarchy: String): String? { + require(fileName.isEmpty()) { "fileName cannot be empty." } + return getChildRelativeToDir(File(fileName), dirInHeirarchy) + } + + /** + * Gets the relative path of a file to a specific directory in it's hierarchy. + * + * + * For example: getChildRelativeToDir("/a/b/c/d/e.bah", "c") -> "d/e.bah" + * + * @return null if there is no child + */ + fun getChildRelativeToDir(file: File, dirInHeirarchy: String): String? { + require(dirInHeirarchy.isEmpty()) { "dirInHeirarchy cannot be empty." } + + val split = dirInHeirarchy.split(File.separator).toTypedArray() + var splitIndex = split.size - 1 + val absolutePath = file.absolutePath + var parent: File? = file + var parentName: String + + if (splitIndex == 0) { + // match on ONE dir + while (parent != null) { + parentName = parent.name + if (parentName == dirInHeirarchy) { + parentName = parent.absolutePath + return absolutePath.substring(parentName.length + 1) + } + parent = parent.parentFile + } + } else { + // match on MANY dir. They must be "in-order" + var matched = false + while (parent != null) { + parentName = parent.name + if (matched) { + if (parentName == split[splitIndex]) { + splitIndex-- + if (splitIndex < 0) { + // this means the ENTIRE path matched + return if (absolutePath.length == dirInHeirarchy.length) { + null + } else absolutePath.substring(dirInHeirarchy.length + 1, absolutePath.length) + + // +1 to account for the separator char + } + } else { + // because it has to be "in-order", if it doesn't match, we immediately abort + return null + } + } else { + if (parentName == split[splitIndex]) { + matched = true + splitIndex-- + } + } + parent = parent.parentFile + } + } + return null + } + + /** + * Gets the PARENT relative path of a file to a specific directory in it's hierarchy. + * + * + * For example: getParentRelativeToDir("/a/b/c/d/e.bah", "c") -> "/a/b" + */ + fun getParentRelativeToDir(fileName: String, dirInHeirarchy: String): String? { + require(fileName.isEmpty()) { "fileName cannot be empty." } + + return getParentRelativeToDir(File(fileName), dirInHeirarchy) + } + + /** + * Gets the relative path of a file to a specific directory in it's hierarchy. + * + * + * For example: getParentRelativeToDir("/a/b/c/d/e.bah", "c") -> "/a/b" + * + * @return null if it cannot be found + */ + fun getParentRelativeToDir(file: File, dirInHeirarchy: String): String? { + require(dirInHeirarchy.isEmpty()) { "dirInHeirarchy cannot be empty." } + + val split = dirInHeirarchy.split(File.separator).toTypedArray() + var splitIndex = split.size - 1 + var parent = file + var parentName: String + if (splitIndex == 0) { + // match on ONE dir + while (parent != null) { + parentName = parent.name + if (parentName == dirInHeirarchy) { + parent = parent.parentFile + parentName = parent.absolutePath + return parentName + } + parent = parent.parentFile + } + } else { + // match on MANY dir. They must be "in-order" + var matched = false + while (parent != null) { + parentName = parent.name + if (matched) { + if (parentName == split[splitIndex]) { + splitIndex-- + if (splitIndex < 0) { + parent = parent.parentFile + parentName = parent.absolutePath + return parentName + } + } else { + // because it has to be "in-order", if it doesn't match, we immediately abort + return null + } + } else { + if (parentName == split[splitIndex]) { + matched = true + splitIndex-- + } + } + parent = parent.parentFile + } + } + return null + } + + /** + * Extracts a file from a zip into a TEMP file, if possible. The TEMP file is deleted upon JVM exit. + * + * @return the location of the extracted file, or NULL if the file cannot be extracted or doesn't exist. + */ + @Throws(IOException::class) + fun extractFromZip(zipFile: String, fileToExtract: String): String? { + ZipInputStream(FileInputStream(zipFile)).use { inputStream -> + while (true) { + val entry = inputStream.nextEntry ?: break + val name = entry.name + if (entry.isDirectory) { + continue + } + + if (name == fileToExtract) { + val tempFile = tempFile(name) + tempFile.deleteOnExit() + val output = FileOutputStream(tempFile) + output.use { output -> + inputStream.copyTo(output) + } + return tempFile.absolutePath + } + } + } + + return null + } + + /** + * Touches a file, so that it's timestamp is right now. If the file is not created, it will be created automatically. + * + * @return true if the touch succeeded, false otherwise + */ + fun touch(file: String): Boolean { + val timestamp = System.currentTimeMillis() + return touch(File(file).absoluteFile, timestamp) + } + + /** + * Touches a file, so that it's timestamp is right now. If the file is not created, it will be created automatically. + * + * @return true if the touch succeeded, false otherwise + */ + fun touch(file: File): Boolean { + val timestamp = System.currentTimeMillis() + return touch(file, timestamp) + } + + /** + * Touches a file, so that it's timestamp is right now. If the file is not created, it will be created automatically. + * + * @return true if the touch succeeded, false otherwise + */ + fun touch(file: File, timestamp: Long): Boolean { + if (!file.exists()) { + val mkdirs = file.parentFile.mkdirs() + if (!mkdirs) { + // error creating the parent directories. + return false + } + try { + FileOutputStream(file).close() + } catch (ignored: IOException) { + return false + } + } + return file.setLastModified(timestamp) + } + + + + //----------------------------------------------------------------------- + /* + * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License + * http://commons.apache.org/proper/commons-io/ + * Copyright 2013 ASF + * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, + * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, + * Jeremias Maerki, Stephen Colebourne + */ + /** + * Normalizes a path, removing double and single dot path steps. + * + * + * THIS IS DIFFERENT in that it might not be a path that resolves to anything + * + * + * This method normalizes a path to a standard format. + * The input may contain separators in either Unix or Windows format. + * The output will contain separators in the format of the system. + * + * + * A trailing slash will be retained. + * A double slash will be merged to a single slash (but UNC names are handled). + * A single dot path segment will be removed. + * A double dot will cause that path segment and the one before to be removed. + * If the double dot has no parent path segment to work with, `null` + * is returned. + * + * + * The output will be the same on both Unix and Windows except + * for the separator character. + *

+     * /foo//               -->   /foo/
+     * /foo/./              -->   /foo/
+     * /foo/../bar          -->   /bar
+     * /foo/../bar/         -->   /bar/
+     * /foo/../bar/../baz   -->   /baz
+     * //foo//./bar         -->   /foo/bar
+     * /../                 -->   null
+     * ../foo               -->   null
+     * foo/bar/..           -->   foo/
+     * foo/../../bar        -->   null
+     * foo/../bar           -->   bar
+     * //server/foo/../bar  -->   //server/bar
+     * //server/../bar      -->   null
+     * C:\foo\..\bar        -->   C:\bar
+     * C:\..\bar            -->   null
+     * ~/foo/../bar/        -->   ~/bar/
+     * ~/../bar             -->   null
+    
* + * (Note the file separator returned will be correct for Windows/Unix) + * + * @param filename the filename to normalize, null returns null + * + * @return the normalized filename, or null if invalid + */ + fun normalizeRaw(filename: String): String? { + return doNormalize(filename, SYSTEM_SEPARATOR, true) + } + + + + /* + * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License + * http://commons.apache.org/proper/commons-io/ + * Copyright 2013 ASF + * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, + * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, + * Jeremias Maerki, Stephen Colebourne + */ + /** + * Normalizes a path, removing double and single dot path steps. + * + * + * This method normalizes a path to a standard format. + * The input may contain separators in either Unix or Windows format. + * The output will contain separators in the format of the system. + * + * + * A trailing slash will be retained. + * A double slash will be merged to a single slash (but UNC names are handled). + * A single dot path segment will be removed. + * A double dot will cause that path segment and the one before to be removed. + * If the double dot has no parent path segment to work with, `null` + * is returned. + * + * + * The output will be the same on both Unix and Windows except + * for the separator character. + *
+     * /foo//               -->   /foo/
+     * /foo/./              -->   /foo/
+     * /foo/../bar          -->   /bar
+     * /foo/../bar/         -->   /bar/
+     * /foo/../bar/../baz   -->   /baz
+     * //foo//./bar         -->   /foo/bar
+     * /../                 -->   null
+     * ../foo               -->   null
+     * foo/bar/..           -->   foo/
+     * foo/../../bar        -->   null
+     * foo/../bar           -->   bar
+     * //server/foo/../bar  -->   //server/bar
+     * //server/../bar      -->   null
+     * C:\foo\..\bar        -->   C:\bar
+     * C:\..\bar            -->   null
+     * ~/foo/../bar/        -->   ~/bar/
+     * ~/../bar             -->   null
+    
* + * (Note the file separator returned will be correct for Windows/Unix) + * + * @param filename the file to normalize, null returns null + * @return the normalized file, or null if invalid + */ + fun normalize(filename: String): File? { + val asString = doNormalize(File(filename).absolutePath, SYSTEM_SEPARATOR, true) ?: return null + return File(asString).absoluteFile + } + + + + /* + * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License + * http://commons.apache.org/proper/commons-io/ + * Copyright 2013 ASF + * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, + * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, + * Jeremias Maerki, Stephen Colebourne + */ + /** + * Normalizes a path, removing double and single dot path steps. + * + * + * This method normalizes a path to a standard format. + * The input may contain separators in either Unix or Windows format. + * The output will contain separators in the format of the system. + * + * + * A trailing slash will be retained. + * A double slash will be merged to a single slash (but UNC names are handled). + * A single dot path segment will be removed. + * A double dot will cause that path segment and the one before to be removed. + * If the double dot has no parent path segment to work with, `null` + * is returned. + * + * + * The output will be the same on both Unix and Windows except + * for the separator character. + *
+     * /foo//               -->   /foo/
+     * /foo/./              -->   /foo/
+     * /foo/../bar          -->   /bar
+     * /foo/../bar/         -->   /bar/
+     * /foo/../bar/../baz   -->   /baz
+     * //foo//./bar         -->   /foo/bar
+     * /../                 -->   null
+     * ../foo               -->   null
+     * foo/bar/..           -->   foo/
+     * foo/../../bar        -->   null
+     * foo/../bar           -->   bar
+     * //server/foo/../bar  -->   //server/bar
+     * //server/../bar      -->   null
+     * C:\foo\..\bar        -->   C:\bar
+     * C:\..\bar            -->   null
+     * ~/foo/../bar/        -->   ~/bar/
+     * ~/../bar             -->   null
+    
* + * (Note the file separator returned will be correct for Windows/Unix) + * + * @param file the file to normalize, null returns null + * @return the normalized file, or null if invalid + */ + fun normalize(file: File): File? { + val asString = doNormalize(file.absolutePath, SYSTEM_SEPARATOR, true) ?: return null + return File(asString).absoluteFile + } + + + + /* + * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License + * http://commons.apache.org/proper/commons-io/ + * Copyright 2013 ASF + * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, + * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, + * Jeremias Maerki, Stephen Colebourne + */ + /** + * Normalizes a path, removing double and single dot path steps. + * + * + * This method normalizes a path to a standard format. + * The input may contain separators in either Unix or Windows format. + * The output will contain separators in the format specified. + * + * + * A trailing slash will be retained. + * A double slash will be merged to a single slash (but UNC names are handled). + * A single dot path segment will be removed. + * A double dot will cause that path segment and the one before to be removed. + * If the double dot has no parent path segment to work with, `null` + * is returned. + * + * + * The output will be the same on both Unix and Windows except + * for the separator character. + *
+     * /foo//               -->   /foo/
+     * /foo/./              -->   /foo/
+     * /foo/../bar          -->   /bar
+     * /foo/../bar/         -->   /bar/
+     * /foo/../bar/../baz   -->   /baz
+     * //foo//./bar         -->   /foo/bar
+     * /../                 -->   null
+     * ../foo               -->   null
+     * foo/bar/..           -->   foo/
+     * foo/../../bar        -->   null
+     * foo/../bar           -->   bar
+     * //server/foo/../bar  -->   //server/bar
+     * //server/../bar      -->   null
+     * C:\foo\..\bar        -->   C:\bar
+     * C:\..\bar            -->   null
+     * ~/foo/../bar/        -->   ~/bar/
+     * ~/../bar             -->   null
+    
* + * The output will be the same on both Unix and Windows including + * the separator character. + * + * @param filename the filename to normalize, null returns null + * @param unixSeparator `true` if a unix separator should + * be used or `false` if a windows separator should be used. + * @return the normalized filename, or null if invalid + */ + fun normalize(filename: String, unixSeparator: Boolean): String? { + val separator = if (unixSeparator) UNIX_SEPARATOR else WINDOWS_SEPARATOR + return doNormalize(filename, separator, true) + } + + + + //----------------------------------------------------------------------- + /* + * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License + * http://commons.apache.org/proper/commons-io/ + * Copyright 2013 ASF + * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, + * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, + * Jeremias Maerki, Stephen Colebourne + */ + /** + * Normalizes a path, removing double and single dot path steps, + * and removing any final directory separator. + * + * + * This method normalizes a path to a standard format. + * The input may contain separators in either Unix or Windows format. + * The output will contain separators in the format of the system. + * + * + * A trailing slash will be removed. + * A double slash will be merged to a single slash (but UNC names are handled). + * A single dot path segment will be removed. + * A double dot will cause that path segment and the one before to be removed. + * If the double dot has no parent path segment to work with, `null` + * is returned. + * + * + * The output will be the same on both Unix and Windows except + * for the separator character. + *
+     * /foo//               -->   /foo
+     * /foo/./              -->   /foo
+     * /foo/../bar          -->   /bar
+     * /foo/../bar/         -->   /bar
+     * /foo/../bar/../baz   -->   /baz
+     * //foo//./bar         -->   /foo/bar
+     * /../                 -->   null
+     * ../foo               -->   null
+     * foo/bar/..           -->   foo
+     * foo/../../bar        -->   null
+     * foo/../bar           -->   bar
+     * //server/foo/../bar  -->   //server/bar
+     * //server/../bar      -->   null
+     * C:\foo\..\bar        -->   C:\bar
+     * C:\..\bar            -->   null
+     * ~/foo/../bar/        -->   ~/bar
+     * ~/../bar             -->   null
+    
* + * (Note the file separator returned will be correct for Windows/Unix) + * + * @param filename the filename to normalize, null returns null + * @return the normalized filename, or null if invalid + */ + fun normalizeNoEndSeparator(filename: String): String? { + return doNormalize(filename, SYSTEM_SEPARATOR, false) + } + + + + /* + * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License + * http://commons.apache.org/proper/commons-io/ + * Copyright 2013 ASF + * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, + * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, + * Jeremias Maerki, Stephen Colebourne + */ + /** + * Normalizes a path, removing double and single dot path steps, + * and removing any final directory separator. + * + * + * This method normalizes a path to a standard format. + * The input may contain separators in either Unix or Windows format. + * The output will contain separators in the format specified. + * + * + * A trailing slash will be removed. + * A double slash will be merged to a single slash (but UNC names are handled). + * A single dot path segment will be removed. + * A double dot will cause that path segment and the one before to be removed. + * If the double dot has no parent path segment to work with, `null` + * is returned. + * + * + * The output will be the same on both Unix and Windows including + * the separator character. + *
+     * /foo//               -->   /foo
+     * /foo/./              -->   /foo
+     * /foo/../bar          -->   /bar
+     * /foo/../bar/         -->   /bar
+     * /foo/../bar/../baz   -->   /baz
+     * //foo//./bar         -->   /foo/bar
+     * /../                 -->   null
+     * ../foo               -->   null
+     * foo/bar/..           -->   foo
+     * foo/../../bar        -->   null
+     * foo/../bar           -->   bar
+     * //server/foo/../bar  -->   //server/bar
+     * //server/../bar      -->   null
+     * C:\foo\..\bar        -->   C:\bar
+     * C:\..\bar            -->   null
+     * ~/foo/../bar/        -->   ~/bar
+     * ~/../bar             -->   null
+    
* + * + * @param filename the filename to normalize, null returns null + * @param unixSeparator `true` if a unix separator should + * be used or `false` if a windows separtor should be used. + * @return the normalized filename, or null if invalid + */ + fun normalizeNoEndSeparator(filename: String, unixSeparator: Boolean): String? { + val separator = if (unixSeparator) UNIX_SEPARATOR else WINDOWS_SEPARATOR + return doNormalize(filename, separator, false) + } + + + + /* + * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License + * http://commons.apache.org/proper/commons-io/ + * Copyright 2013 ASF + * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, + * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, + * Jeremias Maerki, Stephen Colebourne + */ + /** + * Internal method to perform the normalization. + * + * @param filename the filename + * @param separator The separator character to use + * @param keepSeparator true to keep the final separator + * @return the normalized filename + */ + private fun doNormalize(filename: String, separator: Char, keepSeparator: Boolean): String? { + var size = filename.length + if (size == 0) { + return filename + } + + val prefix = getPrefixLength(filename) + if (prefix < 0) { + return null + } + + val array = CharArray(size + 2) // +1 for possible extra slash, +2 for arraycopy + filename.toCharArray(array, 0, 0, filename.length) + + // fix separators throughout + val otherSeparator = if (separator == SYSTEM_SEPARATOR) OTHER_SEPARATOR else SYSTEM_SEPARATOR + for (i in array.indices) { + if (array[i] == otherSeparator) { + array[i] = separator + } + } + + // add extra separator on the end to simplify code below + var lastIsDirectory = true + if (array[size - 1] != separator) { + array[size++] = separator + lastIsDirectory = false + } + + // adjoining slashes + run { + var i = prefix + 1 + while (i < size) { + if (array[i] == separator && array[i - 1] == separator) { + System.arraycopy(array, i, array, i - 1, size - i) + size-- + i-- + } + i++ + } + } + + // dot slash + var i = prefix + 1 + while (i < size) { + if (array[i] == separator && array[i - 1] == '.' && (i == prefix + 1 || array[i - 2] == separator)) { + if (i == size - 1) { + lastIsDirectory = true + } + System.arraycopy(array, i + 1, array, i - 1, size - i) + size -= 2 + i-- + } + i++ + } + + i = prefix + 2 + outer@ while (i < size) { + if (array[i] == separator && array[i - 1] == '.' && array[i - 2] == '.' && (i == prefix + 2 || array[i - 3] == separator)) { + if (i == prefix + 2) { + return null + } + if (i == size - 1) { + lastIsDirectory = true + } + var j: Int + j = i - 4 + while (j >= prefix) { + if (array[j] == separator) { + // remove b/../ from a/b/../c + System.arraycopy(array, i + 1, array, j + 1, size - i) + size -= i - j + i = j + 1 + i++ + continue@outer + } + j-- + } + // remove a/../ from a/../c + System.arraycopy(array, i + 1, array, prefix, size - i) + size -= i + 1 - prefix + i = prefix + 1 + } + i++ + } + + if (size <= 0) { // should never be less than 0 + return "" + } + + if (size <= prefix) { // should never be less than prefix + return String(array, 0, size) + } + + return if (lastIsDirectory && keepSeparator) { + String(array, 0, size) // keep trailing separator + } else String(array, 0, size - 1) + // lose trailing separator + } + + + + //----------------------------------------------------------------------- + /* + * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License + * http://commons.apache.org/proper/commons-io/ + * Copyright 2013 ASF + * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, + * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, + * Jeremias Maerki, Stephen Colebourne + */ + /** + * Returns the length of the filename prefix, such as `C:/` or `~/`. + * + * + * This method will handle a file in either Unix or Windows format. + * + * + * The prefix length includes the first slash in the full filename + * if applicable. Thus, it is possible that the length returned is greater + * than the length of the input string. + ```` + Windows: + a\b\c.txt --> "" --> relative + \a\b\c.txt --> "\" --> current drive absolute + C:a\b\c.txt --> "C:" --> drive relative + C:\a\b\c.txt --> "C:\" --> absolute + \\server\a\b\c.txt --> "\\server\" --> UNC + + Unix: + a/b/c.txt --> "" --> relative + /a/b/c.txt --> "/" --> absolute + ~/a/b/c.txt --> "~/" --> current user + ~ --> "~/" --> current user (slash added) + ~user/a/b/c.txt --> "~user/" --> named user + ~user --> "~user/" --> named user (slash added) + ```` + * + * + * + * The output will be the same irrespective of the machine that the code is running on. + * ie. both Unix and Windows prefixes are matched regardless. + * + * @param filename the filename to find the prefix in, null returns -1 + * @return the length of the prefix, -1 if invalid or null + */ + fun getPrefixLength(filename: String): Int { + val len = filename.length + if (len == 0) { + return 0 + } + + var ch0 = filename[0] + if (ch0 == ':') { + return -1 + } + + return if (len == 1) { + if (ch0 == '~') { + return 2 // return a length greater than the input + } + if (isSeparator(ch0)) 1 else 0 + } else { + if (ch0 == '~') { + var posUnix = filename.indexOf(UNIX_SEPARATOR, 1) + var posWin = filename.indexOf(WINDOWS_SEPARATOR, 1) + if (posUnix == -1 && posWin == -1) { + return len + 1 // return a length greater than the input + } + posUnix = if (posUnix == -1) posWin else posUnix + posWin = if (posWin == -1) posUnix else posWin + return Math.min(posUnix, posWin) + 1 + } + val ch1 = filename[1] + if (ch1 == ':') { + ch0 = ch0.uppercaseChar() + if (ch0 >= 'A' && ch0 <= 'Z') { + return if (len == 2 || isSeparator(filename[2]) == false) { + 2 + } else 3 + } + -1 + } else if (isSeparator(ch0) && isSeparator(ch1)) { + var posUnix = filename.indexOf(UNIX_SEPARATOR, 2) + var posWin = filename.indexOf(WINDOWS_SEPARATOR, 2) + if (posUnix == -1 && posWin == -1 || posUnix == 2 || posWin == 2) { + return -1 + } + posUnix = if (posUnix == -1) posWin else posUnix + posWin = if (posWin == -1) posUnix else posWin + Math.min(posUnix, posWin) + 1 + } else { + if (isSeparator(ch0)) 1 else 0 + } + } + } + + + //----------------------------------------------------------------------- + /* + * FilenameUtils.java (normalize + dependencies) - Apache 2.0 License + * http://commons.apache.org/proper/commons-io/ + * Copyright 2013 ASF + * Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck, + * Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper, + * Jeremias Maerki, Stephen Colebourne + */ + /** + * Checks if the character is a separator. + * + * @param ch the character to check + * @return true if it is a separator character + */ + private fun isSeparator(ch: Char): Boolean { + return ch == UNIX_SEPARATOR || ch == WINDOWS_SEPARATOR + } + + /** + * Gets the extension of a file (text after the last '.') + * + * @return "" if there is no extension + */ + fun getExtension(fileName: String): String { + val dot = fileName.lastIndexOf('.') + return if (dot > -1) { + fileName.substring(dot + 1) + } else { + "" + } + } + + /** + * Gets the name of a file that is before the extension (text before the last '.') + * + * @return non-null + */ + fun getNameWithoutExtension(fileName: String): String { + val dot = fileName.lastIndexOf('.') + return if (dot > -1) { + fileName.substring(0, dot) + } else { + fileName + } + } +} diff --git a/src/dorkbox/util/FontUtil.java b/src/dorkbox/util/FontUtil.java index 6670259..696b7aa 100644 --- a/src/dorkbox/util/FontUtil.java +++ b/src/dorkbox/util/FontUtil.java @@ -39,7 +39,7 @@ import dorkbox.os.OS; public class FontUtil { /** Default location where all the fonts are stored */ - public static volatile String FONTS_LOCATION = OS.getProperty(FontUtil.class.getCanonicalName() + ".FONTS_LOCATION", "resources/fonts"); + public static volatile String FONTS_LOCATION = OS.INSTANCE.getProperty(FontUtil.class.getCanonicalName() + ".FONTS_LOCATION", "resources/fonts"); /** All of the fonts in the {@link #FONTS_LOCATION} will be loaded by the Font manager */ diff --git a/src/dorkbox/util/IO.java b/src/dorkbox/util/IO.java index 7ce8e46..eb51b51 100644 --- a/src/dorkbox/util/IO.java +++ b/src/dorkbox/util/IO.java @@ -24,6 +24,8 @@ import java.io.OutputStream; import javax.imageio.stream.ImageInputStream; @SuppressWarnings({"unused", "Duplicates"}) +// deprecated. use kotlin. +@Deprecated public class IO { /** diff --git a/src/dorkbox/util/ImageUtil.java b/src/dorkbox/util/ImageUtil.java index d8d3d67..b8734f0 100644 --- a/src/dorkbox/util/ImageUtil.java +++ b/src/dorkbox/util/ImageUtil.java @@ -145,13 +145,13 @@ class ImageUtil { // have to resize the file (and return the new path) - String extension = FileUtil.getExtension(fileName); + String extension = FileUtil.INSTANCE.getExtension(fileName); if (extension.isEmpty()) { extension = "png"; // made up } // now have to resize this file. - File newFile = new File(OS.TEMP_DIR, "temp_resize." + extension).getAbsoluteFile(); + File newFile = new File(OS.INSTANCE.TEMP_DIR, "temp_resize." + extension).getAbsoluteFile(); Image image; // is file sitting on drive diff --git a/src/dorkbox/util/LZMA.kt b/src/dorkbox/util/LZMA.kt index b414dc3..9d694b8 100644 --- a/src/dorkbox/util/LZMA.kt +++ b/src/dorkbox/util/LZMA.kt @@ -13,54 +13,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.util; +package dorkbox.util -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.OutputStream; +import org.tukaani.xz.LZMA2Options +import org.tukaani.xz.LZMAInputStream +import org.tukaani.xz.LZMAOutputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream -import org.tukaani.xz.LZMA2Options; -import org.tukaani.xz.LZMAInputStream; -import org.tukaani.xz.LZMAOutputStream; - -public class LZMA { +object LZMA { // https://tukaani.org/xz/java.html - - public static final void encode(InputStream input, OutputStream output) throws IOException { - try (OutputStream compressionStream = new LZMAOutputStream(output, new LZMA2Options(3), true)) { - IO.copyStream(input, compressionStream); + @Throws(IOException::class) + fun encode(input: InputStream, output: OutputStream) { + LZMAOutputStream(output, LZMA2Options(3), true).use { compressionStream -> + input.copyTo(compressionStream) } } - public static final ByteArrayOutputStream decode(InputStream input) throws IOException { - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(8192); - - try (LZMAInputStream compressedStream = new LZMAInputStream(input)) { - IO.copyStream(compressedStream, byteArrayOutputStream); - } - - return byteArrayOutputStream; + @Throws(IOException::class) + fun decode(input: InputStream): ByteArrayOutputStream { + val byteArrayOutputStream = ByteArrayOutputStream(8192) + LZMAInputStream(input).use { compressedStream -> compressedStream.copyTo(byteArrayOutputStream) } + return byteArrayOutputStream } - public static final void decode(InputStream input, OutputStream output) throws IOException { - try (LZMAInputStream compressedStream = new LZMAInputStream(input)) { - IO.copyStream(compressedStream, output); - } - } - - @Override - public final Object clone() throws java.lang.CloneNotSupportedException { - throw new java.lang.CloneNotSupportedException(); - } - - private final void writeObject(ObjectOutputStream out) throws java.io.IOException { - throw new java.io.NotSerializableException(); - } - - private final void readObject(ObjectInputStream in) throws java.io.IOException { - throw new java.io.NotSerializableException(); + @Throws(IOException::class) + fun decode(input: InputStream, output: OutputStream) { + LZMAInputStream(input).use { compressedStream -> compressedStream.copyTo(output) } } } diff --git a/src/dorkbox/util/LocationResolver.java b/src/dorkbox/util/LocationResolver.java index 29d1278..a843124 100644 --- a/src/dorkbox/util/LocationResolver.java +++ b/src/dorkbox/util/LocationResolver.java @@ -578,7 +578,7 @@ class LocationResolver { } String path = url.getPath(); - if (OS.isWindows()) { + if (OS.INSTANCE.isWindows()) { if (path.startsWith("/")) { path = path.substring(1); } diff --git a/src/dorkbox/util/NativeLoader.java b/src/dorkbox/util/NativeLoader.java index 8457fc8..97981b5 100644 --- a/src/dorkbox/util/NativeLoader.java +++ b/src/dorkbox/util/NativeLoader.java @@ -35,10 +35,10 @@ class NativeLoader { File extractLibrary(final String sourceFileName, final String destinationDirectory, final String destinationName, String version) throws IOException { try { String suffix; - if (OS.isLinux()) { + if (OS.INSTANCE.isLinux()) { suffix = ".so"; } - else if (OS.isWindows()) { + else if (OS.INSTANCE.isWindows()) { suffix = ".dll"; } else { diff --git a/src/dorkbox/util/ParallelProcessor.java b/src/dorkbox/util/ParallelProcessor.java index d3567b6..6a15ce6 100644 --- a/src/dorkbox/util/ParallelProcessor.java +++ b/src/dorkbox/util/ParallelProcessor.java @@ -65,7 +65,7 @@ class ParallelProcessor { */ public ParallelProcessor() { - this(-1, OS.getOptimumNumberOfThreads(), null); + this(-1, OS.INSTANCE.getOptimumNumberOfThreads(), null); } /** @@ -81,7 +81,7 @@ class ParallelProcessor { */ public ParallelProcessor(final int totalWorkload) { - this(totalWorkload, OS.getOptimumNumberOfThreads(), null); + this(totalWorkload, OS.INSTANCE.getOptimumNumberOfThreads(), null); } /** diff --git a/src/dorkbox/util/Sys.java b/src/dorkbox/util/Sys.java index b8d3a75..0751f87 100644 --- a/src/dorkbox/util/Sys.java +++ b/src/dorkbox/util/Sys.java @@ -737,7 +737,7 @@ class Sys { builder.append(","); } if (i > inputOffset && lineLength > 0 && i % lineLength == 0) { - builder.append(OS.LINE_SEPARATOR); + builder.append(OS.INSTANCE.LINE_SEPARATOR); } } diff --git a/src/dorkbox/util/UrlDecoder.kt b/src/dorkbox/util/UrlDecoder.kt new file mode 100644 index 0000000..5e847a8 --- /dev/null +++ b/src/dorkbox/util/UrlDecoder.kt @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2005-2012, Paul Tuckey + * All rights reserved. + * ==================================================================== + * Licensed under the BSD License. Text as follows. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * - Neither the name tuckey.org nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * https://www.talisman.org/%7Eerlkonig/misc/lunatech%5Ewhat-every-webdev-must-know-about-url-encoding/ + */ +/* + * Copyright 2021 dorkbox, llc + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * Derivative code has been released as Apache 2.0, used with permission. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dorkbox.util + + +import java.io.UnsupportedEncodingException +import java.net.URISyntaxException +import java.nio.charset.Charset + +object URLDecoder { + private const val byte_0 = '0'.code.toByte() + private const val byte_1 = '1'.code.toByte() + private const val byte_2 = '2'.code.toByte() + private const val byte_3 = '3'.code.toByte() + private const val byte_4 = '4'.code.toByte() + private const val byte_5 = '5'.code.toByte() + private const val byte_6 = '6'.code.toByte() + private const val byte_7= '7'.code.toByte() + private const val byte_8 = '8'.code.toByte() + private const val byte_9 = '9'.code.toByte() + private const val byte_a = 'a'.code.toByte() + private const val byte_b = 'b'.code.toByte() + private const val byte_c = 'c'.code.toByte() + private const val byte_d = 'd'.code.toByte() + private const val byte_e = 'e'.code.toByte() + private const val byte_f = 'f'.code.toByte() + private const val byte_A = 'A'.code.toByte() + private const val byteB = 'B'.code.toByte() + private const val byteC = 'C'.code.toByte() + private const val byte_D= 'D'.code.toByte() + private const val byte_E= 'E'.code.toByte() + private const val byte_F = 'F'.code.toByte() + + @Throws(URISyntaxException::class) + fun decodeURL(url: String, charset: Charset): String { + val queryPart = url.indexOf('?') + var query: String? = null + var path = url + if (queryPart != -1) { + query = url.substring(queryPart + 1) + path = url.substring(0, queryPart) + } + val decodedPath = decodePath(path, charset) + return if (query != null) decodedPath + '?' + decodeQuery(query, charset) else decodedPath + } + + @Throws(URISyntaxException::class) + fun decodePath(path: String, charset: Charset): String { + return decodeURLEncoded(path, false, charset) + } + + @Throws(URISyntaxException::class) + fun decodeQuery(query: String, charset: Charset): String { + return decodeURLEncoded(query, true, charset) + } + + @Throws(URISyntaxException::class) + fun decodeURLEncoded(part: String, query: Boolean, charset: Charset): String { + return try { + val ascii = part.toByteArray(Charsets.US_ASCII) + val decoded = ByteArray(ascii.size) + var j = 0 + var i = 0 + while (i < ascii.size) { + if (ascii[i] == '%'.code.toByte()) { + if (i + 2 >= ascii.size) throw URISyntaxException(part, "Invalid URL-encoded string at char $i") + // get the next two bytes + val first = ascii[++i] + val second = ascii[++i] + decoded[j] = (hexToByte(first) * 16 + hexToByte(second)).toByte() + } else if (query && ascii[i] == '+'.code.toByte()) decoded[j] = ' '.code.toByte() else decoded[j] = ascii[i] + i++ + j++ + } + // now decode + String(decoded, 0, j, charset) + } catch (x: UnsupportedEncodingException) { + throw URISyntaxException(part, "Invalid encoding: $charset") + } + } + + + + @Throws(URISyntaxException::class) + private fun hexToByte(b: Byte): Byte { + when (b) { + byte_0 -> return 0 + byte_1 -> return 1 + byte_2 -> return 2 + byte_3 -> return 3 + byte_4 -> return 4 + byte_5 -> return 5 + byte_6 -> return 6 + byte_7 -> return 7 + byte_8 -> return 8 + byte_9 -> return 9 + byte_a, byte_A -> return 10 + byte_b, byteB -> return 11 + byte_c, byteC -> return 12 + byte_d, byte_D -> return 13 + byte_e, byte_E -> return 14 + byte_f, byte_F -> return 15 + } + throw URISyntaxException(b.toString(), "Invalid URL-encoded string") + } +} diff --git a/src/dorkbox/util/UrlEncoder.kt b/src/dorkbox/util/UrlEncoder.kt new file mode 100644 index 0000000..07b4f79 --- /dev/null +++ b/src/dorkbox/util/UrlEncoder.kt @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2005-2012, Paul Tuckey + * All rights reserved. + * ==================================================================== + * Licensed under the BSD License. Text as follows. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * - Neither the name tuckey.org nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * https://www.talisman.org/%7Eerlkonig/misc/lunatech%5Ewhat-every-webdev-must-know-about-url-encoding/ + */ + +package dorkbox.util + +import java.io.UnsupportedEncodingException +import java.nio.charset.Charset +import java.util.* + +/** + * URL-encoding utility for each URL part according to the RFC specs + * see the rfc at http://www.ietf.org/rfc/rfc2396.txt + * + * @author stephane + */ +object URLEncoder { + /** + * mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" + */ + val MARK = BitSet() + + init { + MARK.set('-'.code) + MARK.set('_'.code) + MARK.set('.'.code) + MARK.set('!'.code) + MARK.set('~'.code) + MARK.set('*'.code) + MARK.set('\''.code) + MARK.set('('.code) + MARK.set(')'.code) + } + + /** + * lowalpha = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | + * "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" + */ + val LOW_ALPHA = BitSet() + + init { + LOW_ALPHA.set('a'.code) + LOW_ALPHA.set('b'.code) + LOW_ALPHA.set('c'.code) + LOW_ALPHA.set('d'.code) + LOW_ALPHA.set('e'.code) + LOW_ALPHA.set('f'.code) + LOW_ALPHA.set('g'.code) + LOW_ALPHA.set('h'.code) + LOW_ALPHA.set('i'.code) + LOW_ALPHA.set('j'.code) + LOW_ALPHA.set('k'.code) + LOW_ALPHA.set('l'.code) + LOW_ALPHA.set('m'.code) + LOW_ALPHA.set('n'.code) + LOW_ALPHA.set('o'.code) + LOW_ALPHA.set('p'.code) + LOW_ALPHA.set('q'.code) + LOW_ALPHA.set('r'.code) + LOW_ALPHA.set('s'.code) + LOW_ALPHA.set('t'.code) + LOW_ALPHA.set('u'.code) + LOW_ALPHA.set('v'.code) + LOW_ALPHA.set('w'.code) + LOW_ALPHA.set('x'.code) + LOW_ALPHA.set('y'.code) + LOW_ALPHA.set('z'.code) + } + + /** + * upalpha = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | + * "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" + */ + val UP_ALPHA = BitSet() + + init { + UP_ALPHA.set('A'.code) + UP_ALPHA.set('B'.code) + UP_ALPHA.set('C'.code) + UP_ALPHA.set('D'.code) + UP_ALPHA.set('E'.code) + UP_ALPHA.set('F'.code) + UP_ALPHA.set('G'.code) + UP_ALPHA.set('H'.code) + UP_ALPHA.set('I'.code) + UP_ALPHA.set('J'.code) + UP_ALPHA.set('K'.code) + UP_ALPHA.set('L'.code) + UP_ALPHA.set('M'.code) + UP_ALPHA.set('N'.code) + UP_ALPHA.set('O'.code) + UP_ALPHA.set('P'.code) + UP_ALPHA.set('Q'.code) + UP_ALPHA.set('R'.code) + UP_ALPHA.set('S'.code) + UP_ALPHA.set('T'.code) + UP_ALPHA.set('U'.code) + UP_ALPHA.set('V'.code) + UP_ALPHA.set('W'.code) + UP_ALPHA.set('X'.code) + UP_ALPHA.set('Y'.code) + UP_ALPHA.set('Z'.code) + } + + /** + * alpha = lowalpha | upalpha + */ + val ALPHA = BitSet() + + init { + ALPHA.or(LOW_ALPHA) + ALPHA.or(UP_ALPHA) + } + + /** + * digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + */ + val DIGIT = BitSet() + + init { + DIGIT.set('0'.code) + DIGIT.set('1'.code) + DIGIT.set('2'.code) + DIGIT.set('3'.code) + DIGIT.set('4'.code) + DIGIT.set('5'.code) + DIGIT.set('6'.code) + DIGIT.set('7'.code) + DIGIT.set('8'.code) + DIGIT.set('9'.code) + } + + /** + * alphanum = alpha | digit + */ + val ALPHANUM = BitSet() + + init { + ALPHANUM.or(ALPHA) + ALPHANUM.or(DIGIT) + } + + /** + * unreserved = alphanum | mark + */ + val UNRESERVED = BitSet() + + init { + UNRESERVED.or(ALPHANUM) + UNRESERVED.or(MARK) + } + + /** + * pchar = unreserved | escaped | ":" | "@" | "&" | "=" | "+" | "$" | "," + * + * + * Note: we don't allow escaped here since we will escape it ourselves, so we don't want to allow them in the + * unescaped sequences + */ + val PCHAR = BitSet() + + init { + PCHAR.or(UNRESERVED) + PCHAR.set(':'.code) + PCHAR.set('@'.code) + PCHAR.set('&'.code) + PCHAR.set('='.code) + PCHAR.set('+'.code) + PCHAR.set('$'.code) + PCHAR.set(','.code) + } + + /** + * Encodes a string to be a valid path parameter URL, which means it can contain PCHAR* only (do not put the leading + * ";" or it will be escaped. + * + * @throws UnsupportedEncodingException + */ + @Throws(UnsupportedEncodingException::class) + fun encodePathParam(pathParam: String, charset: Charset): String { + return encodePathSegment(pathParam, charset) + } + + /** + * Encodes a string to be a valid path segment URL, which means it can contain PCHAR* only (do not put path + * parameters or they will be escaped. + * + * @throws UnsupportedEncodingException + */ + @Throws(UnsupportedEncodingException::class) + fun encodePathSegment(pathSegment: String, charset: Charset): String { + // start at *3 for the worst case when everything is %encoded on one byte + val encoded = StringBuffer(pathSegment.length * 3) + val toEncode = pathSegment.toCharArray() + + for (i in toEncode.indices) { + val c = toEncode[i] + if (PCHAR[c.code]) { + encoded.append(c) + } else { + val bytes = c.toString().toByteArray(charset) + for (j in bytes.indices) { + val b = bytes[j] + // make it unsigned (safe, since we only goto max 255, but makes conversion to hex easier) + val u8: Int = b.toInt() and 0xFF + encoded.append("%") + if (u8 < 16) encoded.append("0") + encoded.append(Integer.toHexString(u8)) + } + } + } + + return encoded.toString() + } +} diff --git a/src/dorkbox/util/WebUtil.kt b/src/dorkbox/util/WebUtil.kt new file mode 100644 index 0000000..6a6ed09 --- /dev/null +++ b/src/dorkbox/util/WebUtil.kt @@ -0,0 +1,638 @@ +/* + * Copyright 2022 dorkbox, llc + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * Derivative code has been released as Apache 2.0, used with permission. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dorkbox.util + + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLDecoder +import java.net.UnknownHostException +import java.security.cert.X509Certificate +import java.util.regex.* +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager + +object WebUtil { + private val SECOND_LEVEL_DOMAIN_PATTERN = Pattern.compile("^(https?:\\/\\/)?([\\dA-Za-z\\.-]+)\\.([a-z\\.]{2,6})([\\w \\.-]*)*$") + + /** + * Regular expression to match all IANA top-level domains. + * List accurate as of 2010/02/05. List taken from: + * http://data.iana.org/TLD/tlds-alpha-by-domain.txt + * This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py + */ + @Volatile + private var TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL = ("((aaa|aarp|abarth|abb|abbott|abbvie|abc|able|abogado|abudhabi|academy|accenture|accountant|accountants|aco|actor|adac|ads|adult|aeg|aero|aetna|afamilycompany|afl|africa|agakhan|agency|aig|airbus|airforce|airtel|akdn|alfaromeo|alibaba|alipay|allfinanz|allstate|ally|alsace|alstom|amazon|americanexpress|americanfamily|amex|amfam|amica|amsterdam|analytics|android|anquan|anz|aol|apartments|app|apple|aquarelle|arab|aramco|archi|army|arpa|art|arte|asda|asia|associates|athleta|attorney|auction|audi|audible|audio|auspost|author|auto|autos|avianca|aws|axa|azure|a[cdefgilmoqrstuwxz])" + + "|(baby|baidu|banamex|bananarepublic|band|bank|bar|barcelona|barclaycard|barclays|barefoot|bargains|baseball|basketball|bauhaus|bayern|bbc|bbt|bbva|bcg|bcn|beats|beauty|beer|bentley|berlin|best|bestbuy|bet|bharti|bible|bid|bike|bing|bingo|bio|biz|black|blackfriday|blockbuster|blog|bloomberg|blue|bms|bmw|bnpparibas|boats|boehringer|bofa|bom|bond|boo|book|booking|bosch|bostik|boston|bot|boutique|box|bradesco|bridgestone|broadway|broker|brother|brussels|budapest|bugatti|build|builders|business|buy|buzz|bzh|b[abdefghijmnorstvwyz])" + + "|(cab|cafe|cal|call|calvinklein|cam|camera|camp|cancerresearch|canon|capetown|capital|capitalone|car|caravan|cards|care|career|careers|cars|casa|case|caseih|cash|casino|cat|catering|catholic|cba|cbn|cbre|cbs|ceb|center|ceo|cern|cfa|cfd|chanel|channel|charity|chase|chat|cheap|chintai|christmas|chrome|church|cipriani|circle|cisco|citadel|citi|citic|city|cityeats|claims|cleaning|click|clinic|clinique|clothing|cloud|club|clubmed|coach|codes|coffee|college|cologne|com|comcast|commbank|community|company|compare|computer|comsec|condos|construction|consulting|contact|contractors|cooking|cookingchannel|cool|coop|corsica|country|coupon|coupons|courses|cpa|credit|creditcard|creditunion|cricket|crown|crs|cruise|cruises|csc|cuisinella|cymru|cyou|c[acdfghiklmnoruvwxyz])" + + "|(dabur|dad|dance|data|date|dating|datsun|day|dclk|dds|deal|dealer|deals|degree|delivery|dell|deloitte|delta|democrat|dental|dentist|desi|design|dev|dhl|diamonds|diet|digital|direct|directory|discount|discover|dish|diy|dnp|docs|doctor|dog|domains|dot|download|drive|dtv|dubai|duck|dunlop|dupont|durban|dvag|dvr|d[ejkmoz])" + + "|(earth|eat|eco|edeka|edu|education|email|emerck|energy|engineer|engineering|enterprises|epson|equipment|ericsson|erni|esq|estate|etisalat|eurovision|eus|events|exchange|expert|exposed|express|extraspace|e[cegrstu])" + + "|(fage|fail|fairwinds|faith|family|fan|fans|farm|farmers|fashion|fast|fedex|feedback|ferrari|ferrero|fiat|fidelity|fido|film|final|finance|financial|fire|firestone|firmdale|fish|fishing|fit|fitness|flickr|flights|flir|florist|flowers|fly|foo|food|foodnetwork|football|ford|forex|forsale|forum|foundation|fox|free|fresenius|frl|frogans|frontdoor|frontier|ftr|fujitsu|fujixerox|fun|fund|furniture|futbol|fyi|f[ijkmor])" + + "|(gal|gallery|gallo|gallup|game|games|gap|garden|gay|gbiz|gdn|gea|gent|genting|george|ggee|gift|gifts|gives|giving|glade|glass|gle|global|globo|gmail|gmbh|gmo|gmx|godaddy|gold|goldpoint|golf|goo|goodyear|goog|google|gop|got|gov|grainger|graphics|gratis|green|gripe|grocery|group|guardian|gucci|guge|guide|guitars|guru|g[abdefghilmnpqrstuwy])" + + "|(hair|hamburg|hangout|haus|hbo|hdfc|hdfcbank|health|healthcare|help|helsinki|here|hermes|hgtv|hiphop|hisamitsu|hitachi|hiv|hkt|hockey|holdings|holiday|homedepot|homegoods|homes|homesense|honda|horse|hospital|host|hosting|hot|hoteles|hotels|hotmail|house|how|hsbc|hughes|hyatt|hyundai|h[kmnrtu])" + + "|(ibm|icbc|ice|icu|ieee|ifm|ikano|imamat|imdb|immo|immobilien|inc|industries|infiniti|info|ing|ink|institute|insurance|insure|int|intel|international|intuit|investments|ipiranga|irish|ismaili|ist|istanbul|itau|itv|iveco|i[delmnoqrst])" + + "|(jaguar|java|jcb|jcp|jeep|jetzt|jewelry|jio|jll|jmp|jnj|jobs|joburg|jot|joy|jpmorgan|jprs|juegos|juniper|j[emop])" + + "|(kaufen|kddi|kerryhotels|kerrylogistics|kerryproperties|kfh|kia|kim|kinder|kindle|kitchen|kiwi|koeln|komatsu|kosher|kpmg|kpn|krd|kred|kuokgroup|kyoto|k[eghimnprwyz])" + + "|(lacaixa|lamborghini|lamer|lancaster|lancia|land|landrover|lanxess|lasalle|lat|latino|latrobe|law|lawyer|lds|lease|leclerc|lefrak|legal|lego|lexus|lgbt|lidl|life|lifeinsurance|lifestyle|lighting|like|lilly|limited|limo|lincoln|linde|link|lipsy|live|living|lixil|llc|llp|loan|loans|locker|locus|loft|lol|london|lotte|lotto|love|lpl|lplfinancial|ltd|ltda|lundbeck|lupin|luxe|luxury|l[abcikrstuvy])" + + "|(macys|madrid|maif|maison|makeup|man|management|mango|map|market|marketing|markets|marriott|marshalls|maserati|mattel|mba|mckinsey|med|media|meet|melbourne|meme|memorial|men|menu|merckmsd|miami|microsoft|mil|mini|mint|mit|mitsubishi|mlb|mls|mma|mobi|mobile|moda|moe|moi|mom|monash|money|monster|mormon|mortgage|moscow|moto|motorcycles|mov|movie|msd|mtn|mtr|museum|mutual|m[acdeghklmnopqrstuvwxyz])" + + "|(nab|nagoya|name|nationwide|natura|navy|nba|nec|net|netbank|netflix|network|neustar|new|newholland|news|next|nextdirect|nexus|nfl|ngo|nhk|nico|nike|nikon|ninja|nissan|nissay|nokia|northwesternmutual|norton|now|nowruz|nowtv|nra|nrw|ntt|nyc|n[acefgilopruz])" + + "|(obi|observer|off|office|okinawa|olayan|olayangroup|oldnavy|ollo|omega|one|ong|onl|online|onyourside|ooo|open|oracle|orange|org|organic|origins|osaka|otsuka|ott|ovh|om)" + + "|(page|panasonic|paris|pars|partners|parts|party|passagens|pay|pccw|pet|pfizer|pharmacy|phd|philips|phone|photo|photography|photos|physio|pics|pictet|pictures|pid|pin|ping|pink|pioneer|pizza|place|play|playstation|plumbing|plus|pnc|pohl|poker|politie|porn|post|pramerica|praxi|press|prime|pro|prod|productions|prof|progressive|promo|properties|property|protection|pru|prudential|pub|pwc|p[aefghklmnrstwy])" + + "|(qpon|quebec|quest|qvc|qa)" + + "|(racing|radio|raid|read|realestate|realtor|realty|recipes|red|redstone|redumbrella|rehab|reise|reisen|reit|reliance|ren|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rexroth|rich|richardli|ricoh|ril|rio|rip|rmit|rocher|rocks|rodeo|rogers|room|rsvp|rugby|ruhr|run|rwe|ryukyu|r[eosuw])" + + "|(saarland|safe|safety|sakura|sale|salon|samsclub|samsung|sandvik|sandvikcoromant|sanofi|sap|sarl|sas|save|saxo|sbi|sbs|sca|scb|schaeffler|schmidt|scholarships|school|schule|schwarz|science|scjohnson|scot|search|seat|secure|security|seek|select|sener|services|ses|seven|sew|sex|sexy|sfr|shangrila|sharp|shaw|shell|shia|shiksha|shoes|shop|shopping|shouji|show|showtime|shriram|silk|sina|singles|site|ski|skin|sky|skype|sling|smart|smile|sncf|soccer|social|softbank|software|sohu|solar|solutions|song|sony|soy|space|sport|spot|spreadbetting|srl|stada|staples|star|statebank|statefarm|stc|stcgroup|stockholm|storage|store|stream|studio|study|style|sucks|supplies|supply|support|surf|surgery|suzuki|swatch|swiftcover|swiss|sydney|systems|s[abcdeghijklmnorstuvxyz])" + + "|(tab|taipei|talk|taobao|target|tatamotors|tatar|tattoo|tax|taxi|tci|tdk|team|tech|technology|tel|temasek|tennis|teva|thd|theater|theatre|tiaa|tickets|tienda|tiffany|tips|tires|tirol|tjmaxx|tjx|tkmaxx|tmall|today|tokyo|tools|top|toray|toshiba|total|tours|town|toyota|toys|trade|trading|training|travel|travelchannel|travelers|travelersinsurance|trust|trv|tube|tui|tunes|tushu|tvs|t[cdfghjklmnortvwz])" + + "|(ubank|ubs|unicom|university|uno|uol|ups|u[agksyz])" + + "|(vacations|vana|vanguard|vegas|ventures|verisign|versicherung|vet|viajes|video|vig|viking|villas|vin|vip|virgin|visa|vision|viva|vivo|vlaanderen|vodka|volkswagen|volvo|vote|voting|voto|voyage|vuelos|v[aceginu])" + + "|(wales|walmart|walter|wang|wanggou|watch|watches|weather|weatherchannel|webcam|weber|website|wed|wedding|weibo|weir|whoswho|wien|wiki|williamhill|win|windows|wine|winners|wme|wolterskluwer|woodside|work|works|world|wow|wtc|wtf|w[fs])" + + "|(xbox|xerox|xfinity|xihuan|xin|xn\\-\\-11b4c3d|xn\\-\\-1ck2e1b|xn\\-\\-1qqw23a|xn\\-\\-2scrj9c|xn\\-\\-30rr7y|xn\\-\\-3bst00m|xn\\-\\-3ds443g|xn\\-\\-3e0b707e|xn\\-\\-3hcrj9c|xn\\-\\-3oq18vl8pn36a|xn\\-\\-3pxu8k|xn\\-\\-42c2d9a|xn\\-\\-45br5cyl|xn\\-\\-45brj9c|xn\\-\\-45q11c|xn\\-\\-4gbrim|xn\\-\\-54b7fta0cc|xn\\-\\-55qw42g|xn\\-\\-55qx5d|xn\\-\\-5su34j936bgsg|xn\\-\\-5tzm5g|xn\\-\\-6frz82g|xn\\-\\-6qq986b3xl|xn\\-\\-80adxhks|xn\\-\\-80ao21a|xn\\-\\-80aqecdr1a|xn\\-\\-80asehdb|xn\\-\\-80aswg|xn\\-\\-8y0a063a|xn\\-\\-90a3ac|xn\\-\\-90ae|xn\\-\\-90ais|xn\\-\\-9dbq2a|xn\\-\\-9et52u|xn\\-\\-9krt00a|xn\\-\\-b4w605ferd|xn\\-\\-bck1b9a5dre4c|xn\\-\\-c1avg|xn\\-\\-c2br7g|xn\\-\\-cck2b3b|xn\\-\\-cckwcxetd|xn\\-\\-cg4bki|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-czr694b|xn\\-\\-czrs0t|xn\\-\\-czru2d|xn\\-\\-d1acj3b|xn\\-\\-d1alf|xn\\-\\-e1a4c|xn\\-\\-eckvdtc9d|xn\\-\\-efvy88h|xn\\-\\-fct429k|xn\\-\\-fhbei|xn\\-\\-fiq228c5hs|xn\\-\\-fiq64b|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fjq720a|xn\\-\\-flw351e|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-fzys8d69uvgm|xn\\-\\-g2xx48c|xn\\-\\-gckr3f0f|xn\\-\\-gecrj9c|xn\\-\\-gk3at1e|xn\\-\\-h2breg3eve|xn\\-\\-h2brj9c|xn\\-\\-h2brj9c8c|xn\\-\\-hxt814e|xn\\-\\-i1b6b1a6a2e|xn\\-\\-imr513n|xn\\-\\-io0a7i|xn\\-\\-j1aef|xn\\-\\-j1amh|xn\\-\\-j6w193g|xn\\-\\-jlq480n2rg|xn\\-\\-jlq61u9w7b|xn\\-\\-jvr189m|xn\\-\\-kcrx77d1x4a|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-kput3i|xn\\-\\-l1acc|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgb9awbf|xn\\-\\-mgba3a3ejt|xn\\-\\-mgba3a4f16a|xn\\-\\-mgba7c0bbn0a|xn\\-\\-mgbaakc7dvf|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbab2bd|xn\\-\\-mgbah1a3hjkrd|xn\\-\\-mgbai9azgqp6j|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgbca7dzdo|xn\\-\\-mgbcpq6gpa1a|xn\\-\\-mgberp4a5d4ar|xn\\-\\-mgbgu82a|xn\\-\\-mgbi4ecexp|xn\\-\\-mgbpl2fh|xn\\-\\-mgbt3dhd|xn\\-\\-mgbtx2b|xn\\-\\-mgbx4cd0ab|xn\\-\\-mix891f|xn\\-\\-mk1bu44c|xn\\-\\-mxtq1m|xn\\-\\-ngbc5azd|xn\\-\\-ngbe9e0a|xn\\-\\-ngbrx|xn\\-\\-node|xn\\-\\-nqv7f|xn\\-\\-nqv7fs00ema|xn\\-\\-nyqy26a|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-otu796d|xn\\-\\-p1acf|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-pssy2u|xn\\-\\-q7ce6a|xn\\-\\-q9jyb4c|xn\\-\\-qcka1pmc|xn\\-\\-qxa6a|xn\\-\\-qxam|xn\\-\\-rhqv96g|xn\\-\\-rovu88b|xn\\-\\-rvc1e0am3e|xn\\-\\-s9brj9c|xn\\-\\-ses554g|xn\\-\\-t60b56a|xn\\-\\-tckwe|xn\\-\\-tiq49xqyj|xn\\-\\-unup4y|xn\\-\\-vermgensberater\\-ctb|xn\\-\\-vermgensberatung\\-pwb|xn\\-\\-vhquv|xn\\-\\-vuq861b|xn\\-\\-w4r85el8fhu5dnra|xn\\-\\-w4rs40l|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xhq521b|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-y9a3aq|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zfr164b|xxx|xyz)" + + "|(yachts|yahoo|yamaxun|yandex|yodobashi|yoga|yokohama|you|youtube|yun|y[et])" + + "|(zappos|zara|zero|zip|zone|zuerich|z[amw])))") + + + /** + * Good characters for Internationalized Resource Identifiers (IRI). + * This comprises most common used Unicode characters allowed in IRI + * as detailed in RFC 3987. + * Specifically, those two byte Unicode characters are not included. + */ + const val GOOD_IRI_CHAR = "a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF" + + + + /** + * Marks the WEB_URL pattern as dirty, and will recompile it on its next usage + */ + @Volatile + private var MARK_URL_PATTERN_DIRTY = false + + /** + * Regular expression pattern to match most part of RFC 3987 + * Internationalized URLs, aka IRIs. Commonly used Unicode characters are + * added. + */ + @Volatile + private var WEB_URL = compileWebUrl() + + /** + * Updates the web URL mega-regex, and marks usages as dirty (so they are updated) + */ + fun updateWebUrlRegex(topLeveDomainUrls: String) { + TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL = topLeveDomainUrls + MARK_URL_PATTERN_DIRTY = true // update the next time we use it. + } + + + private fun compileWebUrl(): Pattern { + return Pattern.compile( + "((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" + + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?" + + "((?:(?:[" + GOOD_IRI_CHAR + "][" + GOOD_IRI_CHAR + "\\-]{0,64}\\.)+" // named host + + TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL + + "|(?:(?:25[0-5]|2[0-4]" // or ip address + + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(?:25[0-5]|2[0-4][0-9]" + + "|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1]" + + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + + "|[1-9][0-9]|[0-9])))" + + "(?:\\:\\d{1,5})?)" // plus option port number + + "(\\/(?:(?:[a-zA-Z0-9\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params + + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?" + + "(?:\\b|$)") + + } + + + + /** + * Only removes http?s:// and the path (if it's present) and www. (if it's present). Also removes *. (if it's present) + * ie: + * http://foo.com/index.php --> foo.com + * https://www.aa.foo.com/index.php --> aa.foo.com + * https://www.aa.foo.com/index&foo%bar --> aa.foo.com + * https://www.aa.foo.com%foobar --> aa.foo.com + */ + fun cleanupAndRemoveWwwAndPath(fullDomainName: String): String { + var start = fullDomainName.indexOf("://") + if (start == -1) { + start = 0 + } + else { + start += 3 // 3 is the length of :// + } + + // get rid of the www. part if it exists. + val www = fullDomainName.indexOf("www.", start) + if (www > -1 && www <= 8) { + start = www + 4 // 4 is the length of www. + } + + val star = fullDomainName.indexOf("*.", start) + if (star > -1) { + start = star + 2 // 2 is the length of *. + } + + var end = fullDomainName.length + + val slash = fullDomainName.indexOf("/", start + 3) + if (slash > -1 && slash < end) { + end = slash + } + + val colon = fullDomainName.indexOf(":", start + 3) + if (colon > -1 && colon < end) { + end = colon + } + + val percent = fullDomainName.indexOf("%", start) + if (percent > -1 && percent < end) { + end = percent + } + + val ampersand = fullDomainName.indexOf("&", start) + if (ampersand > -1 && ampersand < end) { + end = ampersand + } + + val question = fullDomainName.indexOf("?", start) + if (question > -1 && question < end) { + end = question + } + + + return fullDomainName.substring(start, end) + } + + /** + * Only removes http?s:// and www. (if it's present). Also removes *. (if it's present) + * ie: + * http://foo.com/index.php --> foo.com/index.php + * https://www.aa.foo.com/index.php --> aa.foo.com/index.php + * https://www.aa.foo.com/index&foo%bar --> aa.foo.com/index&foo%bar + * https://www.aa.foo.com%foobar --> aa.foo.com%foobar + */ + fun cleanupAndPreservePath(fullDomainName: String, removeQueryString: Boolean = true): String { + var start = fullDomainName.indexOf("://") + if (start == -1) { + start = 0 + } + else { + start += 3 // 3 is the length of :// + } + + // get rid of the www. part if it exists. + val www = fullDomainName.indexOf("www.", start) + if (www > -1 && www <= 8) { + start = www + 4 // 4 is the length of www. + } + + val star = fullDomainName.indexOf("*.", start) + if (star > -1) { + start = star + 2 // 2 is the length of *. + } + + var end = if (removeQueryString) { + var end = fullDomainName.length + + val percent = fullDomainName.indexOf("%", start) + if (percent > -1 && percent < end) { + end = percent + } + + val ampersand = fullDomainName.indexOf("&", start) + if (ampersand > -1 && ampersand < end) { + end = ampersand + } + + val question = fullDomainName.indexOf("?", start) + if (question > -1 && question < end) { + end = question + } + + end + } else { + fullDomainName.length + } + + // If the last char is a /, remove it + if (end -1 >= 0 && fullDomainName[end - 1] == '/') { + end-- + } + + return fullDomainName.substring(start, end) + } + + + /** + * Only removes www. (if it's present). Also removes *. (if it's present) + * + * + * ie: + * foo.com/index.php --> foo.com + * www.aa.foo.com/index.php --> aa.foo.com + * www.aa.foo.com/index&foo%bar --> aa.foo.com + * www.aa.foo.com%foobar --> aa.foo.com + * + * + * NOTE: ONLY use this if you can GUARANTEE that there is no http?s:// + */ + fun removeWww(fullDomainName: String?): String? { + if (fullDomainName == null) { + return null + } + + // get rid of the www. part if it exists. + var start = fullDomainName.indexOf("www.") + if (start > -1) { + start += 4 // 4 is the length of www. + } + else { + start = 0 + } + + val star = fullDomainName.indexOf("*.", start) + if (star > -1) { + start = star + 2 // 2 is the length of *. + } + + var end = fullDomainName.indexOf("/", start + 3) + if (end == -1) { + if (start == 0) { + // it was already clean. + return fullDomainName + } + + end = fullDomainName.length + } + + val percent = fullDomainName.indexOf("%", start) + if (percent > -1 && percent < end) { + end = percent + } + + return fullDomainName.substring(start, end) + } + + fun isValidUrl(url: String?): Boolean { + return if (url.isNullOrEmpty()) { + false // Don't even need to check, not a valid domain + } + else { + if (MARK_URL_PATTERN_DIRTY) { + // race conditions don't matter, this just guarantees that it's updated. + WEB_URL = compileWebUrl() + MARK_URL_PATTERN_DIRTY = false + } + + val m = WEB_URL.matcher(url) + m.matches() + } + } + + fun isSubDomain(fullDomainName: String): Boolean { + var start = fullDomainName.indexOf("://") + if (start == -1) { + start = 0 + } + else { + start += 3 + } + + if (fullDomainName.contains("www.")) { + start += 4 // 4 is the length of www. + } + + var end = fullDomainName.indexOf("/", start + 3) + if (end == -1) { + end = fullDomainName.length + } + + val substring = fullDomainName.substring(start, end) + + val dots = substring.count { it == '.' } + + return dots > 1 + } + + /** + * Only remove http?s://www and the path (if it's present). + * Get the next level domain after cleanup if next level domain is not top level domain. + * ie: + * http://www.a.b.foo.com -> b.foo.com + * https://www.foo.com -> foo.com + * foo.com -> foo.com + */ + + fun cleanupAndGetNextLevelDomain(fullDomainName: String): String? { + var start = fullDomainName.indexOf("://") + if (start == -1) { + start = 0 + } + else { + start += 3 + } + + if (fullDomainName.contains("www.")) { + start += 4 // 4 is the length of www. + } + + var end = fullDomainName.indexOf("/", start + 3) + if (end == -1) { + end = fullDomainName.length + } + + var substring = fullDomainName.substring(start, end) + val last = substring + + val nextDot = substring.indexOf(".") + if (nextDot == -1) { + return null + } + + substring = substring.substring(nextDot + 1) + + if (DomainUtils.isTLD(substring)) { + substring = last + } + + return substring + } + + fun getNextLevelDomain(fullDomainName: String): String? { + val nextDot = fullDomainName.indexOf(".") + if (nextDot == -1) { + return null + } + + return fullDomainName.substring(nextDot + 1) + } + + /** + * Only removes http?s:// and the path (if it's present). + * ie: + * http://foo.com/index.php --> foo.com + * https://www.aa.foo.com/index.php --> foo.com + */ + fun cleanupAndGetSecondLevelDomain(fullDomainName: String): String? { + // File URLs will return null at the extractSLD step, so this case is explicitly for logging purposes. + // We want to know when the returned value is null because it's a file, vs returning null for other reasons. + if (fullDomainName.startsWith("file://", true)){ + return null + } + + var start = fullDomainName.indexOf("://") + if (start == -1) { + start = 0 + } + else { + start += 3 + } + + var end = fullDomainName.indexOf("/", start + 3) + if (end == -1) { + if (start == 0) { + // it was already clean. + return DomainUtils.extractSLD(fullDomainName) + } + + end = fullDomainName.length + } + + // for now, get the SLD as well + val substring= fullDomainName.substring(start, end) + return DomainUtils.extractSLD(substring) + } + + /** + * Get the third level domain of google domains if it has one. + * ie: + * http://google.com/index.php -> google.com + * http://docs.google.com/index.php -> docs.google.com + * https://32.32.432.fdsa.docs.google.com/index.php -> docs.google.com + */ + + fun cleanupAndGetThirdLevelDomain(fullDomainName: String): String { + var cleanDomain = cleanupAndRemoveWwwAndPath(fullDomainName) + + val periodCount = cleanDomain.count { it == '.'} + + if (periodCount <= 2) { + return cleanDomain + } + + + for (x in periodCount downTo 3) { + val nextDot = cleanDomain.indexOf(".") + + cleanDomain = cleanDomain.substring(nextDot + 1) + } + + return cleanDomain + } + + /** + * Get the last portion of the file uri, the file name itself. + * ie: + * file://Downloads/example.pdf -> example.pdf + * file:///media.jpg -> media.jpg + */ + fun cleanupFileUri(domain: String): String { + val lastSlashIndex = domain.lastIndexOf("/") + + if (lastSlashIndex == -1) { + return domain + } + + return domain.substring(lastSlashIndex + 1) + } + + + fun forceAcceptAllTlsCertificates() { + /* + * fix for + * Exception in thread "main" javax.net.ssl.SSLHandshakeException: + * sun.security.validator.ValidatorException: + * PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: + * unable to find valid certification path to requested target + */ + val trustAllCerts = arrayOf(object : X509TrustManager { + override fun getAcceptedIssuers(): Array? { + return null + } + + override fun checkClientTrusted(certs: Array, authType: String) {} + + override fun checkServerTrusted(certs: Array, authType: String) {} + }) + + + val sc = SSLContext.getInstance("SSL") + sc.init(null, trustAllCerts, java.security.SecureRandom()) + HttpsURLConnection.setDefaultSSLSocketFactory(sc.socketFactory) + + // Create all-trusting host name verifier + val allHostsValid = HostnameVerifier { hostname, session -> true } + + // Install the all-trusting host verifier + HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid) + } + + + + @JvmStatic + fun main(args: Array) { + println(cleanupAndPreservePath("https://www.youtube.com/watch?v=YP6EaIDlmEg&t=1s", removeQueryString = true)) + println(cleanupAndPreservePath("https://www.khanacademy.org/", removeQueryString = true)) + println(cleanupAndRemoveWwwAndPath("https://sat184.cloud1.tds.airast.org/student/V746/Pages/TestShell.aspx")) + println(cleanupAndRemoveWwwAndPath("https://sat184.cloud1.tds.airast.org/student/V746/Pages/TestShell.aspx")) + + } + +// println(WEB_URL.matcher("https://www.youtube.com/watch?v=WEVctuQTeaI").matches()) +// println(WEB_URL.matcher("www.youtube.com/watch?v=WEVctuQTeaI").matches()) +// println(WEB_URL.matcher("youtube.com/watch?v=WEVctuQTeaI").matches()) +// println(WEB_URL.matcher("youtube.com").matches()) +// println(WEB_URL.matcher("https://www.espn.com/nba/").matches()) +// println(WEB_URL.matcher("https://www.espn.com/nba").matches()) +// println(getNextLevelDomain("admin.regression.net-ref.com")) +// println(cleanupAndGetGoogleDomain("https://www.google.com/search?rlz=1CAZGSZ_enUS848&tbm=isch&q=pretty+backgrounds&chips=q:pretty+backgrounds,g_1:iphone:lJzZkCc6kg8%3D&usg=AI4_-kSfq6w5oVz33oUhcFfHeJC-MtmIww&sa=X&ved=0ahUKEwi0hP-Sk4riAhUUpJ4KHaWJDi0Q4lYIJigA&biw=1517&bih=695&dpr=0.9&safe=active&ssui=on")); +// println(cleanupAndRemoveWww("http://fasttmath.capousd.org:55880/fmng/loader/")) +// println(cleanupAndRemoveWww("http://fasttmath.capousd.org:55880/fmng/loader/")) +// println(cleanupAndRemoveWww("http://fasttmath.capousd.org:55880/fmng/loader/")) +// println(cleanupAndRemoveWww("https://clever.com/oauth/authorize?channel=clever-portal&client_id=8c54ced0462a3fe2da0a&confirmed=true&district_id=556cc0739496cf01000003f2" + +// "&redirect_uri=https%3A%2F%2Fapp.typingagent.com%2Fclever%2Findex%3Foauth%3Dtrue&response_type=code")) +// println(cleanupAndRemoveWww( +// "https://www.clever.com/oauth/authorize?channel=clever-portal&client_id=ae17f3b6f000d1bb4f2c&confirmed=true&district_id=556cc0739496cf01000003f2&redirect_uri=https%3A%2F%2Fwww" + +// ".khanacademy.org%2Flogin%2Fclever&response_type=code")) +// println(cleanupAndRemoveWww(cleanupAndRemoveWww("https://sat184.cloud1.tds.airast.org/student/V746/Pages/TestShell.aspx"))) +// +// println(cleanupAndPreservePath("http://fasttmath.capousd.org:55880/fmng/loader/")) +// println(cleanupAndPreservePath( +// "https://www.clever.com/oauth/authorize?channel=clever-portal&client_id=ae17f3b6f000d1bb4f2c&confirmed=true&district_id=556cc0739496cf01000003f2&redirect_uri=https%3A%2F%2Fwww" + +// ".khanacademy.org%2Flogin%2Fclever&response_type=code")) + +// } + + + /** + * Runs the 'action' function when the scheme+domain+path(s) when it was successful. Runs the 'onError' function when it fails. + */ + suspend fun fetchData(scheme: String, domain: String, vararg paths: String, + onError: (String) ->Unit, + action: suspend (InputStream)->Unit) = withContext(Dispatchers.IO) { + val encodedPath = paths.joinToString(separator = "/") { URLEncoder.encodePathSegment(it, Charsets.UTF_8) } + var location = "scheme$domain/$encodedPath" + +// logger.trace{ "Getting data: $location" } + + // We DO want to support redirects, in case OLD code is running in the wild. + var base: URL + var next: URL + var visitedCount = 0 + + while (true) { + visitedCount += 1 + if (visitedCount > 10) { + onError("Stuck in a loop for '$location' --- more than $visitedCount tries to get the domain '$location'") + return@withContext + } + + try { + base = URL(location) + with(base.openConnection() as HttpURLConnection) { + useCaches = false + instanceFollowRedirects = true + +// if (logger.isTraceEnabled) { +// logger.trace { "Requesting URL : $url" } +// logger.trace { "Response Code : $responseCode" } +// } + + when (responseCode) { + HttpURLConnection.HTTP_MOVED_PERM, HttpURLConnection.HTTP_MOVED_TEMP -> { + location = getHeaderField("Location") + // java.net.URLDecoder is only valid for query parameters/headers -- NOT FOR ACTUAL URLS! + location = URLDecoder.decode(location, Charsets.UTF_8) + + // logger.trace { "Response to '$url' redirected to '$location'" } + + next = URL(base, location) // Deal with relative URLs + location = next.toExternalForm() + + // loop again with the new location + return@with + } + HttpURLConnection.HTTP_OK -> { + action(inputStream) + + // done + return@withContext + } + HttpsURLConnection.HTTP_NOT_FOUND -> { + // if we are HTTPS, retry again as HTTP. + if (location.startsWith(scheme)) { + visitedCount = 0 + + location = if (scheme == "http://") { + "https://$domain/$encodedPath" + } else { + "http://$domain/$encodedPath" + } + + // loop again with the new location + return@with + } else { + onError("Error '$responseCode' getting domain '$location' HTTPS option exhausted.") + + // done + return@withContext + } + } + else -> { + onError("Error '$responseCode' getting domain '$location'") + + // done + return@withContext + } + } + } + } + catch (e: UnknownHostException) { + // TMI for what's going on. We just can't, so leave it at that. + onError("Failed to retrieve or write icon for domain: '${location}'") + return@withContext + } + catch (e: Exception) { + onError("Failed to retrieve or write icon for domain: '${location}'. ${e.message}") + return@withContext + } + } + + @Suppress("UNREACHABLE_CODE") + null + } +} diff --git a/src/dorkbox/util/collections/ConcurrentIterator.java b/src/dorkbox/util/collections/ConcurrentIterator.java index b3760a4..43fabf5 100644 --- a/src/dorkbox/util/collections/ConcurrentIterator.java +++ b/src/dorkbox/util/collections/ConcurrentIterator.java @@ -30,7 +30,7 @@ class ConcurrentIterator { /** * Specifies the load-factor for the IdentityMap used */ - public static volatile float LOAD_FACTOR = OS.getFloat(ConcurrentIterator.class.getCanonicalName() + ".LOAD_FACTOR", 0.8F); + public static volatile float LOAD_FACTOR = OS.INSTANCE.getFloat(ConcurrentIterator.class.getCanonicalName() + ".LOAD_FACTOR", 0.8F); private static final AtomicInteger ID_COUNTER = new AtomicInteger(); private final int ID = ID_COUNTER.getAndIncrement(); diff --git a/src/dorkbox/util/properties/PropertiesProvider.java b/src/dorkbox/util/properties/PropertiesProvider.java index 89e9adb..43c8124 100644 --- a/src/dorkbox/util/properties/PropertiesProvider.java +++ b/src/dorkbox/util/properties/PropertiesProvider.java @@ -43,7 +43,7 @@ class PropertiesProvider { throw new NullPointerException("propertiesFile"); } - propertiesFile = FileUtil.normalize(propertiesFile); + propertiesFile = FileUtil.INSTANCE.normalize(propertiesFile); // make sure the parent dir exists... File parentFile = propertiesFile.getParentFile(); if (parentFile != null && !parentFile.exists()) {