diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c78687 --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +.idea/ +# CMake +cmake-build-debug/ + + + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +### Kotlin template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +### Gradle template +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties +*.iml diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..772313c --- /dev/null +++ b/build.gradle @@ -0,0 +1,104 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' + } +} + +plugins { + id "org.jetbrains.kotlin.jvm" version "1.2.31" +} +apply plugin: 'maven-publish' +apply plugin: 'com.jfrog.bintray' + +ext.kotlinVersion = '1.2.31' + +group 'io.github.atistrcsn' +version '1.0.0' +jar.baseName 'kutils' + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + freeCompilerArgs = ["-Xjsr305=strict"] + jvmTarget = 1.8 + suppressWarnings = true + } +} + +kotlin { + experimental { + coroutines "enable" + } +} + +sourceSets { + main.kotlin.srcDirs += 'src/main/kotlin' + main.java.srcDirs += 'src/main/kotlin' +} + +dependencies { + // Kotlin + compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}") + compile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") + compile("org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.5") + + // Logging + compile("io.github.microutils:kotlin-logging:1.4.9") + compile("org.slf4j:slf4j-api:1.7.25") + + testCompile "org.jetbrains.kotlin:kotlin-test:${kotlinVersion}" + testCompile "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion}" +} + +publishing { + publications { + KutilsBintrayPublication(MavenPublication) { + from components.java + groupId group + artifactId module.name + version project.version + } + } +} + +bintray { + user = bintray_user + key = bintray_key + publications = ['KutilsBintrayPublication'] + pkg { + repo = 'public' + name = 'kutils' + desc = 'Kotlin starter, kotlin utilities' + licenses = ['MIT'] + vcsUrl = 'https://github.com/atistrcsn/kutils.git' + labels = ['kotlin', 'utils'] + publicDownloadNumbers = true + githubRepo = 'atistrcsn/kutils' //Optional Github repository + githubReleaseNotesFile = 'README.md' //Optional Github readme file + version { + name = project.version + desc = bintray.pkg.desc + released = new Date() + vcsTag = project.version + gpg { + sign = true + } + } + } +} + +task cleanBuildPublishArtifactory(group: 'kutils') { + dependsOn 'clean' + dependsOn 'build' + dependsOn 'publishToMavenLocal' + dependsOn 'bintrayUpload' + tasks.findByName('build').mustRunAfter 'clean' + tasks.findByName('publishToMavenLocal').mustRunAfter 'build' + tasks.findByName('bintrayUpload').mustRunAfter 'publishToMavenLocal' +} + +task wrapper(type: Wrapper) { + gradleVersion = '4.6' +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bf3de21 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9ed4166 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + maven { + url "${artifactory_contextUrl}/maven-dev" + credentials { + username = "${artifactory_user}" + password = "${artifactory_password}" + } + } + } +} + +rootProject.name = 'kutils' \ No newline at end of file diff --git a/src/main/kotlin/io/github/kutils/collection.kt b/src/main/kotlin/io/github/kutils/collection.kt new file mode 100644 index 0000000..27406e8 --- /dev/null +++ b/src/main/kotlin/io/github/kutils/collection.kt @@ -0,0 +1,15 @@ +package io.github.kutils + +/** + * @author atistrcsn - 2017 + */ +/** + * Returns the sum of all values produced by [selector] function applied to each element in the collection. + */ +inline fun Iterable.sumFloatBy(selector: (T) -> Float): Float { + var sum: Float = 0f + for (element in this) { + sum += selector(element) + } + return sum +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/kutils/coroutines.kt b/src/main/kotlin/io/github/kutils/coroutines.kt new file mode 100644 index 0000000..a297a8f --- /dev/null +++ b/src/main/kotlin/io/github/kutils/coroutines.kt @@ -0,0 +1,36 @@ +package io.github.kutils + +import kotlinx.coroutines.experimental.CommonPool +import java.util.concurrent.CompletableFuture +import kotlin.coroutines.experimental.Continuation +import kotlin.coroutines.experimental.CoroutineContext +import kotlin.coroutines.experimental.startCoroutine +import kotlin.coroutines.experimental.suspendCoroutine + +/** + * @author atistrcsn - 2017 + */ + +fun future(context: CoroutineContext = CommonPool, block: suspend () -> T): CompletableFuture = + CompletableFutureCoroutine(context).also { block.startCoroutine(completion = it) } + +class CompletableFutureCoroutine(override val context: CoroutineContext) : CompletableFuture(), Continuation { + override fun resume(value: T) { + complete(value) + } + + override fun resumeWithException(exception: Throwable) { + completeExceptionally(exception) + } +} + +suspend fun CompletableFuture.await(): T = + suspendCoroutine { cont: Continuation -> + whenComplete { result, exception -> + if (exception == null) // the future has been completed normally + cont.resume(result) + else // the future has completed with an exception + cont.resumeWithException(exception) + } + } + diff --git a/src/main/kotlin/io/github/kutils/crypt.kt b/src/main/kotlin/io/github/kutils/crypt.kt new file mode 100644 index 0000000..95a76e6 --- /dev/null +++ b/src/main/kotlin/io/github/kutils/crypt.kt @@ -0,0 +1,57 @@ +package io.github.kutils + +import java.security.MessageDigest +import java.util.* +import javax.crypto.Cipher +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +/** + * Encodes the ByteArray into Base64 encoded one. + * @author atistrcsn - 2017 + */ +inline val ByteArray.base64 get() = Base64.getEncoder().encodeToString(this) + +/** + * Encodes the string into Base64 encoded one. + * @author Suresh G (@sur3shg) + */ +inline val String.base64 get() = Base64.getEncoder().encodeToString(toByteArray(Charsets.US_ASCII)) + +/** + * Decodes the base64 string. + * @author Suresh G (@sur3shg) + */ +inline val String.base64Decode get() = base64DecodeBytes.toString(Charsets.US_ASCII) + +inline val String.md5x16 + get() = MessageDigest.getInstance("MD5").apply { update(toByteArray()) }.digest() + +/** + * Encrypt this string with HMAC-SHA1 using the specified [key]. + * + * @author Suresh G (@sur3shg) + * @param key Encryption key + * @return Encrypted output + */ +fun String.hmacSHA1(key: String): ByteArray { + val mac = Mac.getInstance("HmacSHA1") + mac.init(SecretKeySpec(key.toByteArray(Charsets.UTF_8), "HmacSHA1")) + return mac.doFinal(toByteArray(Charsets.UTF_8)) +} + +/** + * Encrypt this string with AES-128 using the specified [key]. + * Ported from - https://goo.gl/J1H3e5 + * + * @author Suresh G (@sur3shg) + * @param key Encryption key. + * @return Encrypted output. + */ +fun String.aes128Encrypt(key: String): ByteArray { + val nkey = key.normalizeString(16) + val msg = rightPadString('{', 16) + val cipher = Cipher.getInstance("AES/ECB/NoPadding") + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(nkey.toByteArray(Charsets.UTF_8), "AES")) + return cipher.doFinal(msg.toByteArray(Charsets.UTF_8)) +} diff --git a/src/main/kotlin/io/github/kutils/currency.kt b/src/main/kotlin/io/github/kutils/currency.kt new file mode 100644 index 0000000..608303e --- /dev/null +++ b/src/main/kotlin/io/github/kutils/currency.kt @@ -0,0 +1,17 @@ +package io.github.kutils + +import java.text.NumberFormat +import java.util.* + +/** + * @author atistrcsn - 2017 + */ +fun Number.formatWithCurrency(locale: Locale? = null): String = numberFormat(locale).format(this) + +private fun numberFormat(locale: Locale? = null): NumberFormat = NumberFormat.getCurrencyInstance(locale ?: hunLocale).apply { + isGroupingUsed = true + if (locale == null) maximumFractionDigits = 0 +} + +fun Number.formatWithUSD(): String = numberFormat(Locale.US).format(this) +fun Number.formatWithEUR(): String = numberFormat(Locale.FRANCE).format(this) diff --git a/src/main/kotlin/io/github/kutils/datetime.kt b/src/main/kotlin/io/github/kutils/datetime.kt new file mode 100644 index 0000000..b4a2550 --- /dev/null +++ b/src/main/kotlin/io/github/kutils/datetime.kt @@ -0,0 +1,46 @@ +package io.github.kutils + +import java.time.DayOfWeek +import java.time.LocalDateTime +import java.time.Month +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.format.TextStyle +import java.time.temporal.Temporal +import java.util.* +import javax.xml.datatype.DatatypeFactory +import javax.xml.datatype.XMLGregorianCalendar + +/** + * @author atistrcsn - 2017 + */ +val hunZone: ZoneId = ZoneId.of("Europe/Budapest") + +val hunLocale: Locale = Locale("hu", "HU") + +val hunDatePattern = "yyyy-MM-dd" +val hunTimePattern = "HH:mm" +val hunDateTimePattern = "$hunDatePattern $hunTimePattern" + +val hunDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern(hunDatePattern, hunLocale) +val hunTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern(hunTimePattern, hunLocale) +val hunDateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern(hunDateTimePattern, hunLocale) + +fun hunDateFormatter(pattern: String): DateTimeFormatter = DateTimeFormatter.ofPattern(pattern, hunLocale) +fun hunTimeFormatter(pattern: String): DateTimeFormatter = DateTimeFormatter.ofPattern(pattern, hunLocale) +fun hunDateTimeFormatter(pattern: String): DateTimeFormatter = DateTimeFormatter.ofPattern(pattern, hunLocale) + +fun Temporal.local(pattern: String): String = hunDateTimeFormatter(pattern).format(this) + +fun Month.narrow(): String = getDisplayName(TextStyle.NARROW, hunLocale) +fun Month.short(): String = getDisplayName(TextStyle.SHORT, hunLocale) +fun Month.full(): String = getDisplayName(TextStyle.FULL, hunLocale) +fun DayOfWeek.narrow(): String = getDisplayName(TextStyle.NARROW, hunLocale) +fun DayOfWeek.short(): String = getDisplayName(TextStyle.SHORT, hunLocale) +fun DayOfWeek.full(): String = getDisplayName(TextStyle.FULL, hunLocale) + +fun XMLGregorianCalendar.toLocalDateTime(zoneId: ZoneId = hunZone): LocalDateTime = + LocalDateTime.ofInstant(toGregorianCalendar().toInstant(), zoneId) + +fun LocalDateTime.toXmlGregorianCalendar(): XMLGregorianCalendar = + GregorianCalendar.from(atZone(hunZone)).let { DatatypeFactory.newInstance().newXMLGregorianCalendar(it) } \ No newline at end of file diff --git a/src/main/kotlin/io/github/kutils/helpers.kt b/src/main/kotlin/io/github/kutils/helpers.kt new file mode 100644 index 0000000..7a55f57 --- /dev/null +++ b/src/main/kotlin/io/github/kutils/helpers.kt @@ -0,0 +1,374 @@ +package io.github.kutils + +import sun.misc.HexDumpEncoder +import java.io.File +import java.io.IOException +import java.net.JarURLConnection +import java.net.URL +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.security.MessageDigest +import java.util.* +import java.util.concurrent.ThreadLocalRandom +import java.util.jar.Attributes +import java.util.jar.Manifest +import kotlin.reflect.KClass +import kotlin.reflect.KMutableProperty0 +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty0 +import kotlin.reflect.KProperty1 + +/** + * @author atistrcsn - 2017 + */ + +fun Random.nextInt(range: IntRange): Int = range.start + nextInt(range.last - range.start) + +fun rand(range: IntRange): Int = ThreadLocalRandom.current().nextInt(range) + +fun Array.rand(): T = get(Random().nextInt(0..size)) + +/** + * + * + * + * + * Common extension functions. + * + * @author Suresh G (@sur3shg) + */ +const val SPACE = " " + +val LINE_SEP = System.lineSeparator() + +val FILE_SEP = File.separator + +/** + * Prints the [Any.toString] to console. + */ +inline val Any?.p get() = println(this) + +/** + * Pseudo Random number generator. + */ +val RAND = Random(System.nanoTime()) + +/** + * Prepend an empty string of size [col] to the string. + * + * Doesn't preserve original line endings. + */ +fun String.indent(col: Int) = prependIndent(SPACE.repeat(col)) + +/** + * Prepend an empty string of size [col] to each string in the list by skipping first [skip] strings. + * + * @param skip number of head elements to skip from indentation. + * Default to 0 if it's out of bound of list size [0..size] + */ +fun List.indent(col: Int, skip: Int = 0): List { + val skipCount = if (skip in 0..size) skip else 0 + return mapIndexed { idx, str -> if (idx < skipCount) str else str.indent(col) } +} + +/** + * Convert [Byte] to hex. '0x100' OR is used to preserve the leading zero in case of single hex digit. + */ +val Byte.hex get() = Integer.toHexString(toInt() and 0xFF or 0x100).substring(1, 3).toUpperCase() + +/** + * Convert [Byte] to octal. '0x200' OR is used to preserve the leading zero in case of two digit octal. + */ +val Byte.oct get() = Integer.toOctalString(toInt() and 0xFF or 0x200).substring(1, 4) + +/** + * Convert [ByteArray] to hex. + */ +val ByteArray.hex get() = map(Byte::hex).joinToString(" ") + +/** + * Convert [ByteArray] into the classic: "Hexadecimal Dump". + */ +val ByteArray.hexDump get() = HexDumpEncoder().encode(this) + +/** + * Convert [ByteArray] to octal + */ +val ByteArray.oct get() = map(Byte::oct).joinToString(" ") + +/** + * Hex and Octal util methods for Int and Byte + */ +val Int.hex get() = Integer.toHexString(this).toUpperCase() + +val Int.oct get() = Integer.toOctalString(this) + +val Byte.hi get() = toInt() and 0xF0 shr 4 + +val Byte.lo get() = toInt() and 0x0F + +/** + * Convert string to hex. + */ +val String.hex: String get() = toByteArray(Charsets.UTF_8).hex + +/** + * Convert String to octal + */ +val String.oct: String get() = toByteArray(Charsets.UTF_8).oct + +/** + * IPV4 regex pattern + */ +val ip_regex = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$".toRegex() + +val String.isIPv4 get() = matches(ip_regex) + +/** + * Create an MD5 hash of a string. + */ +val String.md5 get() = hash(toByteArray(Charsets.UTF_8), "MD5") + +/** + * Create an SHA1 hash of a string. + */ +val String.sha1 get() = hash(toByteArray(Charsets.UTF_8), "SHA-1") + +/** + * Create an SHA256 hash of a string. + */ +val String.sha256 get() = hash(toByteArray(Charsets.UTF_8), "SHA-256") + +/** + * Decodes the base64 string to byte array. It removes all extra spaces in the + * input string before doing the base64 decode operation. + */ +inline val String.base64DecodeBytes: ByteArray + get() { + val str = replace("\\s+".toRegex(), "") + return Base64.getDecoder().decode(str) + } + +/** + * Trim the ASCII whitespace. + */ +fun String.trimAsciiWhitespace() = trim('\t', '\n', '\u000C', '\r', ' ') + +/** + * Create an MD5 hash of [ByteArray]. + */ +val ByteArray.md5 get() = hash(this, "MD5") + +/** + * Create an SHA1 hash of [ByteArray]. + */ +val ByteArray.sha1 get() = hash(this, "SHA-1") + +/** + * Create an SHA256 hash of [ByteArray]. + */ +val ByteArray.sha256 get() = hash(this, "SHA-256") + +/** + * Returns the byte size of the common binary suffixes. + */ +inline val Int.KB get() = this * 1024L +inline val Int.MB get() = this.KB * 1024 +inline val Int.GB get() = this.MB * 1024 +inline val Int.TB get() = this.GB * 1024 + +/** + * Returns human readable binary prefix for multiples of bytes. + * + * @param si [true] if it's SI unit, else it will be treated as Binary Unit. + */ +fun Long.toBinaryPrefixString(si: Boolean = false): String { + // SI and Binary Units + val unit = if (si) 1_000 else 1_024 + return when { + this < unit -> "$this B" + else -> { + val (prefix, suffix) = when (si) { + true -> "kMGTPEZY" to "B" + false -> "KMGTPEZY" to "iB" + } + // Get only the integral part of the decimal + val exp = (Math.log(this.toDouble()) / Math.log(unit.toDouble())).toInt() + // Binary Prefix mnemonic that is prepended to the units. + val binPrefix = "${prefix[exp - 1]}$suffix" + // Count => (unit^0.x * unit^exp)/unit^exp + String.format("%.2f %s", this / Math.pow(unit.toDouble(), exp.toDouble()), binPrefix) + } + } +} + +/** + * Returns human readable binary prefix for multiples of bytes. + * + * @param si [true] if it's SI unit, else it will be treated as Binary Unit. + */ +fun Int.toBinaryPrefixString(si: Boolean = false) = toLong().toBinaryPrefixString(si) + +/** + * Get the root cause by walks through the exception chain to the last element, + * "root" of the tree, using [Throwable.getCause], and returns that exception. + */ +val Throwable?.rootCause: Throwable? + get() { + var cause = this + while (cause?.cause != null) { + cause = cause.cause + } + return cause + } + +/** + * Find the [msg] hash using the given hashing [algo] + */ +private fun hash(msg: ByteArray, algo: String): String { + val md = MessageDigest.getInstance(algo) + md.reset() + md.update(msg) + val msgDigest = md.digest() + return msgDigest.hex +} + +/** + * Pad this String to a desired multiple on the right using a specified character. + * + * @param padding Padding character. + * @param multipleOf Number which the length must be a multiple of. + */ +fun String.rightPadString(padding: Char, multipleOf: Int): String { + if (isEmpty()) throw IllegalArgumentException("Must supply non-empty string") + if (multipleOf < 2) throw IllegalArgumentException("Multiple ($multipleOf) must be greater than one.") + val needed = multipleOf - (length % multipleOf) + return padEnd(length + needed, padding) +} + +/** + * Normalize a string to a desired length by repeatedly appending itself and/or truncating. + * + * @param desiredLength Desired length of string. + */ +fun String.normalizeString(desiredLength: Int): String { + if (isEmpty()) throw IllegalArgumentException("Must supply non-empty string") + if (desiredLength < 0) throw IllegalArgumentException("Desired length ($desiredLength) must be greater than zero.") + var buf = this + if (length < desiredLength) { + buf = repeat(desiredLength / length + 1) + } + return buf.substring(0, desiredLength) +} + +/** + * Centers the String in a larger String of size [size]. + * Uses [padChar] as the value to pad the String. + */ +fun String.center(size: Int, padChar: Char = ' ') = when { + size > length -> { + val pads = (size - length) / 2 + padStart(length + pads, padChar).padEnd(size, padChar) + } + else -> this +} + +/** + * Deletes the files or directory (recursively) represented by this path. + */ +fun Path.delete() { + if (Files.notExists(this)) { + return + } + if (Files.isDirectory(this)) { + Files.walkFileTree(this, object : SimpleFileVisitor() { + override fun visitFile(file: Path, attrs: BasicFileAttributes) = let { + Files.delete(file) + FileVisitResult.CONTINUE + } + + override fun postVisitDirectory(dir: Path, exc: IOException) = let { + Files.delete(dir) + FileVisitResult.CONTINUE + } + }) + } else { + Files.delete(this) + } +} + +/** + * Exits the system with [msg] + */ +fun exit(status: Int, msg: (() -> String)? = null) { + if (msg != null) { + println(msg()) + } + System.exit(status) +} + +/** + * Returns the jar [Manifest] of the class. Returns [null] if the class + * is not bundled in a jar (Classes in an unpacked class hierarchy). + */ +inline val KClass.jarManifest: Manifest? + get() { + val res = java.getResource("${java.simpleName}.class") + val conn = res.openConnection() + return if (conn is JarURLConnection) conn.manifest else null + } + +/** + * Returns the jar url of the class. Returns the class file url + * if the class is not bundled in a jar. + */ +inline val KClass.jarFileURL: URL + get() { + val res = java.getResource("${java.simpleName}.class") + val conn = res.openConnection() + return if (conn is JarURLConnection) conn.jarFileURL else conn.url + } + +/** + * Common build info attributes + */ +enum class BuildInfo(val attr: String) { + Author("Built-By"), + Date("Built-Date"), + JDK("Build-Jdk"), + Target("Build-Target"), + OS("Build-OS"), + KotlinVersion("Kotlin-Version"), + CreatedBy("Created-By"), + Title(Attributes.Name.IMPLEMENTATION_TITLE.toString()), + Vendor(Attributes.Name.IMPLEMENTATION_VENDOR.toString()), + AppVersion(Attributes.Name.IMPLEMENTATION_VERSION.toString()) +} + +/** + * Returns the [BuildInfo] attribute value from jar manifest [Attributes] + */ +fun Attributes?.getVal(name: BuildInfo): String = this?.getValue(name.attr) ?: "N/A" + +/** + * Add property delegates which call get/set on the given KProperty reference + * + * var foo: String by ::fooImpl + * + * var fooImpl: String + * get() = ... + * set(value) { ... } + * + * @see - https://youtrack.jetbrains.com/issue/KT-8658 + */ +operator fun KProperty0.getValue(instance: Nothing?, metadata: KProperty<*>): R = get() + +operator fun KMutableProperty0.setValue(instance: Nothing?, metadata: KProperty<*>, value: R) = set(value) + +operator fun KProperty1.getValue(instance: T, metadata: KProperty<*>): R = get(instance) + +operator fun KMutableProperty1.setValue(instance: T, metadata: KProperty<*>, value: R) = set(instance, value) \ No newline at end of file diff --git a/src/main/kotlin/io/github/kutils/logger.kt b/src/main/kotlin/io/github/kutils/logger.kt new file mode 100644 index 0000000..6969d32 --- /dev/null +++ b/src/main/kotlin/io/github/kutils/logger.kt @@ -0,0 +1,18 @@ +package io.github.kutils + +/** + * @author atistrcsn - 2017 + */ + +import mu.KLogger +import mu.KotlinLogging +import kotlin.reflect.full.companionObject + +inline fun R.logging(): KLogger = KotlinLogging.logger { } + +fun unwrapCompanionClass(ofClass: Class): Class<*> = + if (ofClass.enclosingClass != null && ofClass.enclosingClass.kotlin.companionObject?.java == ofClass) { + ofClass.enclosingClass + } else { + ofClass + } \ No newline at end of file diff --git a/src/main/kotlin/io/github/kutils/string.kt b/src/main/kotlin/io/github/kutils/string.kt new file mode 100644 index 0000000..fd5362b --- /dev/null +++ b/src/main/kotlin/io/github/kutils/string.kt @@ -0,0 +1,8 @@ +package io.github.kutils + +/** + * @author atistrcsn - 2017 + */ +fun String.truncate(noOfChars: Int): String = substring(0, Math.min(length, noOfChars)) + +fun String.truncateEllips(noOfChars: Int): String = if (length > noOfChars) truncate(noOfChars).dropLast(3).plus("...") else this