diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ef2303..12cc69f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security - No security issues fixed! +## [4.0.0] - 2023-11-07 +### Changed +- BREAKING CHANGE: Bump Gradle version to 8 and AGP version to 8.1.2. +- Update dependencies. + ## [3.4.2] - 2023-08-28 ### Fixed - Fix tags not being sent as String JSON array. _Thanks to [@bogdanzurac](https://github.com/bogdanzurac) for the contribution!_ @@ -478,7 +483,8 @@ res_dir_path -> resDirPath ### Added - Initial release. -[Unreleased]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/3.4.2...HEAD +[Unreleased]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.0.0...HEAD +[4.0.0]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/3.4.2...4.0.0 [3.4.2]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/3.4.1...3.4.2 [3.4.1]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/3.4.0...3.4.1 [3.4.0]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/3.3.0...3.4.0 diff --git a/README.md b/README.md index 0b7e4dd..482e593 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This plug-in super-charges your Android project by providing tasks to download y It also provides a built-in syntax to handle placeholders to enhance the already awesome Android support from PoEditor. ## Minimum requirements -* Android Gradle Plug-in 7.0 or above +* Android Gradle Plug-in 8.0 or above ## Setting Up In your main `build.gradle`, add [jitpack.io](https://jitpack.io/) repository in the `buildscript` block and include the plug-in as a dependency: diff --git a/build.gradle.kts b/build.gradle.kts index 638b0bb..de43667 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -64,8 +64,8 @@ dependencies { } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) + targetCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) withJavadocJar() withSourcesJar() @@ -79,7 +79,7 @@ tasks.javadoc { detekt { toolVersion = libs.versions.detekt.get() - config = files("${project.rootDir}/config/detekt.yml") + config.setFrom(files("${project.rootDir}/config/detekt.yml")) autoCorrect = true } @@ -94,9 +94,10 @@ tasks { maxHeapSize = "1G" } - withType { - // Target version of the generated JVM bytecode. It is used for type resolution. - this.jvmTarget = "1.8" + withType().configureEach { + kotlinOptions { + jvmTarget = libs.versions.java.sdk.get() + } } val installGitHooks by registering(Copy::class) { @@ -169,18 +170,16 @@ publishing { } } } - -// https://github.com/ben-manes/gradle-versions-plugin -// Disallow release candidates as upgradable versions from stable versions -fun String.isNonStable(): Boolean { - val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { toUpperCase().contains(it) } - val regex = "^[0-9,.v-]+(-r)?$".toRegex() - val isStable = stableKeyword || regex.matches(this) - return isStable.not() -} - +// Get only stable versions when running dependencyUpdates tasks.withType { + fun isNonStable(version: String): Boolean { + val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) } + val regex = "^[0-9,.v-]+(-r)?$".toRegex() + val isStable = stableKeyword || regex.matches(version) + return isStable.not() + } + rejectVersionIf { - candidate.version.isNonStable() && !currentVersion.isNonStable() + isNonStable(this.candidate.version) && !isNonStable(this.currentVersion) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c6f7443..c392acc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,14 @@ [versions] -kotlin = "1.8.20-RC" -detekt = "1.22.0" -moshi = "1.14.0" +java-sdk = "17" + +kotlin = "1.9.10" +detekt = "1.23.3" +moshi = "1.15.0" retrofit = "2.9.0" -okhttp = "4.10.0" +okhttp = "4.12.0" [libraries] -android-buildTools = "com.android.tools.build:gradle:7.3.0" +android-buildTools = "com.android.tools.build:gradle:8.1.2" kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } @@ -30,7 +32,7 @@ detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-form [plugins] detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } gitVersionGradle = { id = "com.gladed.androidgitversion", version = "0.4.14"} -versionsUpdate = { id = "com.github.ben-manes.versions", version = "0.46.0" } +versionsUpdate = { id = "com.github.ben-manes.versions", version = "0.49.0" } [bundles] moshi = ["moshi", "moshi-kotlin", "moshi-adapters"] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 31cca49..fce403e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index af33bc8..b3d69a3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,4 @@ * limitations under the License. */ -enableFeaturePreview("VERSION_CATALOGS") - rootProject.name = "poeditor-android-gradle-plugin" diff --git a/src/main/kotlin/com/hyperdevs/poeditor/gradle/PoEditorStringsImporter.kt b/src/main/kotlin/com/hyperdevs/poeditor/gradle/PoEditorStringsImporter.kt index c302aea..7892d60 100644 --- a/src/main/kotlin/com/hyperdevs/poeditor/gradle/PoEditorStringsImporter.kt +++ b/src/main/kotlin/com/hyperdevs/poeditor/gradle/PoEditorStringsImporter.kt @@ -18,6 +18,7 @@ package com.hyperdevs.poeditor.gradle +import com.hyperdevs.poeditor.gradle.adapters.PoEditorDateJsonAdapter import com.hyperdevs.poeditor.gradle.ktx.downloadUrlToString import com.hyperdevs.poeditor.gradle.network.PoEditorApiControllerImpl import com.hyperdevs.poeditor.gradle.network.api.ExportType @@ -30,7 +31,6 @@ import com.hyperdevs.poeditor.gradle.utils.logger import com.hyperdevs.poeditor.gradle.xml.AndroidXmlWriter import com.hyperdevs.poeditor.gradle.xml.XmlPostProcessor import com.squareup.moshi.Moshi -import com.squareup.moshi.adapters.PoEditorDateJsonAdapter import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient diff --git a/src/main/kotlin/com/hyperdevs/poeditor/gradle/adapters/Iso8601Utils.kt b/src/main/kotlin/com/hyperdevs/poeditor/gradle/adapters/Iso8601Utils.kt new file mode 100644 index 0000000..e57d418 --- /dev/null +++ b/src/main/kotlin/com/hyperdevs/poeditor/gradle/adapters/Iso8601Utils.kt @@ -0,0 +1,265 @@ +/* + * Copyright 2023 HyperDevs + * + * 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 com.hyperdevs.poeditor.gradle.adapters + +import com.squareup.moshi.JsonDataException +import java.util.* +import kotlin.math.min +import kotlin.math.pow + +/** + * Jackson’s date formatter, pruned to Moshi's needs. Forked from this file: + * https://github.com/FasterXML/jackson-databind/blob/67ebf7305f492285a8f9f4de31545f5f16fc7c3a/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java + * + * + * Utilities methods for manipulating dates in iso8601 format. This is much, much faster and GC + * friendly than using SimpleDateFormat so highly suitable if you (un)serialize lots of date + * objects. + * + * + * Supported parse format: + * [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:]mm]] + * + * @see [this specification](http://www.w3.org/TR/NOTE-datetime) + */ +internal object Iso8601Utils { + /** ID to represent the 'GMT' string */ + const val GMT_ID = "GMT" + + /** The GMT timezone, prefetched to avoid more lookups. */ + val TIMEZONE_Z = TimeZone.getTimeZone(GMT_ID) + + /** Returns `date` formatted as yyyy-MM-ddThh:mm:ss.sssZ */ + fun format(date: Date?): String { + val calendar: Calendar = GregorianCalendar(TIMEZONE_Z, Locale.US) + calendar.time = date + + // estimate capacity of buffer as close as we can (yeah, that's pedantic ;) + val capacity = "yyyy-MM-ddThh:mm:ss.sssZ".length + val formatted = StringBuilder(capacity) + padInt(formatted, calendar[Calendar.YEAR], "yyyy".length) + formatted.append('-') + padInt(formatted, calendar[Calendar.MONTH] + 1, "MM".length) + formatted.append('-') + padInt(formatted, calendar[Calendar.DAY_OF_MONTH], "dd".length) + formatted.append('T') + padInt(formatted, calendar[Calendar.HOUR_OF_DAY], "hh".length) + formatted.append(':') + padInt(formatted, calendar[Calendar.MINUTE], "mm".length) + formatted.append(':') + padInt(formatted, calendar[Calendar.SECOND], "ss".length) + formatted.append('.') + padInt(formatted, calendar[Calendar.MILLISECOND], "sss".length) + formatted.append('Z') + return formatted.toString() + } + + /** + * Parse a date from ISO-8601 formatted string. It expects a format + * [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]] + * + * @param date ISO string to parse in the appropriate format. + * @return the parsed date + */ + @Suppress("LongMethod", "CyclomaticComplexMethod") + fun parse(date: String): Date { + return try { + var offset = 0 + + // extract year + val year = parseInt(date, offset, 4.let { offset += it; offset }) + if (checkOffset(date, offset, '-')) { + offset += 1 + } + + // extract month + val month = parseInt(date, offset, 2.let { offset += it; offset }) + if (checkOffset(date, offset, '-')) { + offset += 1 + } + + // extract day + val day = parseInt(date, offset, 2.let { offset += it; offset }) + // default time value + var hour = 0 + var minutes = 0 + var seconds = 0 + var milliseconds = 0 // always use 0 otherwise returned date will include millis of current time + + // if the value has no time component (and no time zone), we are done + val hasT = checkOffset(date, offset, 'T') + if (!hasT && date.length <= offset) { + val calendar: Calendar = GregorianCalendar(year, month - 1, day) + return calendar.time + } + if (hasT) { + // extract hours, minutes, seconds and milliseconds + hour = parseInt(date, 1.let { offset += it; offset }, 2.let { offset += it; offset }) + if (checkOffset(date, offset, ':')) { + offset += 1 + } + minutes = parseInt(date, offset, 2.let { offset += it; offset }) + if (checkOffset(date, offset, ':')) { + offset += 1 + } + // second and milliseconds can be optional + if (date.length > offset) { + val c = date[offset] + if (c != 'Z' && c != '+' && c != '-') { + seconds = parseInt(date, offset, 2.let { offset += it; offset }) + if (seconds > 59 && seconds < 63) seconds = 59 // truncate up to 3 leap seconds + // milliseconds can be optional in the format + if (checkOffset(date, offset, '.')) { + offset += 1 + val endOffset = indexOfNonDigit(date, offset + 1) // assume at least one digit + val parseEndOffset = min(endOffset.toDouble(), (offset + 3).toDouble()).toInt() // parse up to 3 digits + val fraction = parseInt(date, offset, parseEndOffset) + milliseconds = (10.toDouble().pow((3 - (parseEndOffset - offset)).toDouble()) * fraction).toInt() + offset = endOffset + } + } + } + } + + // extract timezone + require(date.length > offset) { "No time zone indicator" } + val timezone: TimeZone + val timezoneIndicator = date[offset] + if (timezoneIndicator == 'Z') { + timezone = TIMEZONE_Z + } else if (timezoneIndicator == '+' || timezoneIndicator == '-') { + val timezoneOffset = date.substring(offset) + // 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00" + if ("+0000" == timezoneOffset || "+00:00" == timezoneOffset) { + timezone = TIMEZONE_Z + } else { + // 18-Jun-2015, tatu: Looks like offsets only work from GMT, not UTC... + // not sure why, but it is what it is. + val timezoneId = GMT_ID + timezoneOffset + timezone = TimeZone.getTimeZone(timezoneId) + val act = timezone.id + if (act != timezoneId) { + /* 22-Jan-2015, tatu: Looks like canonical version has colons, but we may be given + * one without. If so, don't sweat. + * Yes, very inefficient. Hopefully not hit often. + * If it becomes a perf problem, add 'loose' comparison instead. + */ + val cleaned = act.replace(":", "") + if (cleaned != timezoneId) { + throw IndexOutOfBoundsException( + "Mismatching time zone indicator: " + + timezoneId + + " given, resolves to " + + timezone.id) + } + } + } + } else { + throw IndexOutOfBoundsException( + "Invalid time zone indicator '$timezoneIndicator'") + } + val calendar: Calendar = GregorianCalendar(timezone) + calendar.isLenient = false + calendar[Calendar.YEAR] = year + calendar[Calendar.MONTH] = month - 1 + calendar[Calendar.DAY_OF_MONTH] = day + calendar[Calendar.HOUR_OF_DAY] = hour + calendar[Calendar.MINUTE] = minutes + calendar[Calendar.SECOND] = seconds + calendar[Calendar.MILLISECOND] = milliseconds + calendar.time + // If we get a ParseException it'll already have the right message/offset. + // Other exception types can convert here. + } catch (e: IndexOutOfBoundsException) { + throw JsonDataException("Not an RFC 3339 date: $date", e) + } catch (e: IllegalArgumentException) { + throw JsonDataException("Not an RFC 3339 date: $date", e) + } + } + + /** + * Check if the expected character exist at the given offset in the value. + * + * @param value the string to check at the specified offset + * @param offset the offset to look for the expected character + * @param expected the expected character + * @return true if the expected character exist at the given offset + */ + private fun checkOffset(value: String, offset: Int, expected: Char): Boolean { + return offset < value.length && value[offset] == expected + } + + /** + * Parse an integer located between 2 given offsets in a string + * + * @param value the string to parse + * @param beginIndex the start index for the integer in the string + * @param endIndex the end index for the integer in the string + * @return the int + * @throws NumberFormatException if the value is not a number + */ + @Throws(NumberFormatException::class) private fun parseInt(value: String, beginIndex: Int, endIndex: Int): Int { + if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) { + throw NumberFormatException(value) + } + // use same logic as in Integer.parseInt() but less generic we're not supporting negative values + var i = beginIndex + var result = 0 + var digit: Int + if (i < endIndex) { + digit = Character.digit(value[i++], 10) + if (digit < 0) { + throw NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex)) + } + result = -digit + } + while (i < endIndex) { + digit = Character.digit(value[i++], 10) + if (digit < 0) { + throw NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex)) + } + result *= 10 + result -= digit + } + return -result + } + + /** + * Zero pad a number to a specified length + * + * @param buffer buffer to use for padding + * @param value the integer value to pad if necessary. + * @param length the length of the string we should zero pad + */ + private fun padInt(buffer: StringBuilder, value: Int, length: Int) { + val strValue = value.toString() + for (i in length - strValue.length downTo 1) { + buffer.append('0') + } + buffer.append(strValue) + } + + /** + * Returns the index of the first character in the string that is not a digit, starting at offset. + */ + private fun indexOfNonDigit(string: String, offset: Int): Int { + for (i in offset until string.length) { + val c = string[i] + if (c < '0' || c > '9') return i + } + return string.length + } +} diff --git a/src/main/kotlin/com/squareup/moshi/adapters/PoEditorDateJsonAdapter.kt b/src/main/kotlin/com/hyperdevs/poeditor/gradle/adapters/PoEditorDateJsonAdapter.kt similarity index 96% rename from src/main/kotlin/com/squareup/moshi/adapters/PoEditorDateJsonAdapter.kt rename to src/main/kotlin/com/hyperdevs/poeditor/gradle/adapters/PoEditorDateJsonAdapter.kt index ecf5ec4..c618919 100644 --- a/src/main/kotlin/com/squareup/moshi/adapters/PoEditorDateJsonAdapter.kt +++ b/src/main/kotlin/com/hyperdevs/poeditor/gradle/adapters/PoEditorDateJsonAdapter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 HyperDevs + * Copyright 2023 HyperDevs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.squareup.moshi.adapters +package com.hyperdevs.poeditor.gradle.adapters import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader