diff --git a/gradle.properties b/gradle.properties index 6ac528f6..8b7ace2a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,5 +3,7 @@ version=4.0.15-SNAPSHOT org.gradle.caching=true org.gradle.parallel=true +versionUpgradePolicy=same_minor_version + compatibilityTestPluginVersion=0.0.3 springFrameworkVersion=6.1.20 \ No newline at end of file diff --git a/gradle/plugins/conventions-plugin/build.gradle b/gradle/plugins/conventions-plugin/build.gradle index 96542e2e..fbcf336f 100644 --- a/gradle/plugins/conventions-plugin/build.gradle +++ b/gradle/plugins/conventions-plugin/build.gradle @@ -5,12 +5,18 @@ plugins { } repositories { + gradlePluginPortal() mavenCentral() } dependencies { + api(platform("org.junit:junit-bom:${junitVersion}")) checkstyle("com.puppycrawl.tools:checkstyle:${checkstyle.toolVersion}") checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}") + + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.assertj:assertj-core:${assertJVersion}") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } gradlePlugin { @@ -23,5 +29,10 @@ gradlePlugin { } dependencies { + implementation("com.github.ben-manes:gradle-versions-plugin:$gradleVersionsPluginVersion") implementation("io.spring.javaformat:spring-javaformat-gradle-plugin:$javaFormatVersion") } + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/JavaPluginConventions.java b/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/JavaPluginConventions.java index d1031f07..0e8a0f84 100644 --- a/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/JavaPluginConventions.java +++ b/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/JavaPluginConventions.java @@ -50,6 +50,7 @@ class JavaPluginConventions { enableSourceAndJavadocJars(java); configureSourceAndTargetCompatibility(java); configureDependencyManagement(project); + configureVersionUpgradePolicy(project); configureJarManifest(project); configureToolchain(project, java); configureJavadocClasspath(project, java); @@ -97,6 +98,10 @@ class JavaPluginConventions { .add(dependencies.enforcedPlatform(dependencies.project(Map.of("path", ":spring-ws-platform")))); } + private void configureVersionUpgradePolicy(Project project) { + new VersionUpgradePluginConventions().apply(project); + } + private void configureJarManifest(Project project) { project.getTasks() .named("jar", Jar.class, diff --git a/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/VersionUpgradePluginConventions.java b/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/VersionUpgradePluginConventions.java new file mode 100644 index 00000000..1e303cc9 --- /dev/null +++ b/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/VersionUpgradePluginConventions.java @@ -0,0 +1,60 @@ +/* + * Copyright 2005-2025 the original author or authors. + * + * 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 + * + * https://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 org.springframework.ws.gradle.conventions; + +import java.util.Locale; + +import com.github.benmanes.gradle.versions.VersionsPlugin; +import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask; +import org.gradle.api.Project; + +import org.springframework.ws.gradle.conventions.support.Version; +import org.springframework.ws.gradle.conventions.support.VersionUpgradePolicy; + +/** + * Conventions for {@code gradle-versions-plugin}. + * + * @author Stephane Nicoll + */ +class VersionUpgradePluginConventions { + + void apply(Project project) { + project.getPlugins().apply(VersionsPlugin.class); + project.getTasks().withType(DependencyUpdatesTask.class, (updateTask) -> { + updateTask.setFilterConfigurations((configuration) -> !(configuration.getName().contains("_") + && configuration.getName().endsWith("+"))); + VersionUpgradePolicy upgradePolicy = getUpgradePolicy(project); + updateTask.rejectVersionIf((candidate) -> { + Version currentVersion = Version.from(candidate.getCurrentVersion()); + Version candidateVersion = Version.from(candidate.getCandidate().getVersion()); + return !upgradePolicy.isCandidate(currentVersion, candidateVersion); + }); + }); + } + + private VersionUpgradePolicy getUpgradePolicy(Project project) { + Object versionUpgradePolicy = project.findProperty("versionUpgradePolicy"); + if (versionUpgradePolicy == null) { + return VersionUpgradePolicy.SAME_MINOR_VERSION; + } + else if (versionUpgradePolicy instanceof String value) { + return VersionUpgradePolicy.valueOf(value.toUpperCase(Locale.ROOT)); + } + throw new IllegalArgumentException("Unsupported version upgrade policy: " + versionUpgradePolicy); + } + +} diff --git a/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/support/Version.java b/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/support/Version.java new file mode 100644 index 00000000..0be97f4b --- /dev/null +++ b/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/support/Version.java @@ -0,0 +1,171 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.ws.gradle.conventions.support; + +import java.io.Serializable; +import java.util.Objects; + +/** + * ë A version representation that provides a major and minor identifier. For instance, + * {@code 1.2.5} would have a {@code major} of "1" and {@code minor} of "1.2". + * + * @author Stephane Nicoll + */ +public class Version { + + private final String id; + + private final String major; + + private final String minor; + + private final Qualifier qualifier; + + private final Parts parts; + + Version(String id, String major, String minor, Qualifier qualifer, Parts parts) { + this.id = id; + this.major = major; + this.minor = minor; + this.qualifier = qualifer; + this.parts = parts; + } + + public static Version from(String version) { + return VersionParser.safeParse(version); + } + + /** + * Return the version. + * @return the version + */ + public String getId() { + return this.id; + } + + /** + * Return the major qualifier or {@code null}. + * @return the major + */ + public String getMajor() { + return this.major; + } + + /** + * Return the minor qualifier or {@code null}. + * @return the minor + */ + public String getMinor() { + return this.minor; + } + + /** + * Return tue {@link Qualifier} or {@code null}. + * @return the qualifier + */ + public Qualifier getQualifier() { + return this.qualifier; + } + + /** + * Return the elements of the version, if any. Does not apply for non-numeric version + * such as a release train. + * @return the parts + */ + public Parts getParts() { + return this.parts; + } + + /** + * Returns whether this version has the same major and minor versions as the + * {@code other} version. + * @param other the version to test + * @return {@code true} if this version has the same major and minor, otherwise + * {@code false} + */ + public boolean isSameMinor(Version other) { + return isSameMajor(other) && Objects.equals(this.parts.minor, other.parts.minor); + } + + /** + * Returns whether this version has the same major version as the {@code other} + * version. + * @param other the version to test + * @return {@code true} if this version has the same major, otherwise {@code false} + */ + public boolean isSameMajor(Version other) { + return (this.parts != null && other.parts != null && Objects.equals(this.parts.major, other.parts.major)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Version version)) { + return false; + } + return Objects.equals(this.id, version.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.id); + } + + @Override + public String toString() { + return this.id; + } + + public record Parts(Integer major, Integer minor, Integer patch, Integer hotPatch) { + + /** + * Return a unique number for this instance that allows to compare two versions. + * @return a comparable number + */ + public long toNumber() { + String paddedValue = paddedNumber(this.major) + paddedNumber(this.minor) + paddedNumber(this.patch) + + paddedNumber(this.hotPatch); + return Long.parseLong(paddedValue); + } + + private String paddedNumber(Integer number) { + if (number != null) { + return String.format("%02d", number); + } + return "00"; + } + + } + + /** + * A version qualifier. + * + * @param id the identifier of the qualifier + * @param version the version or {@code null} + * @param separator the separator + */ + public record Qualifier(String id, Integer version, String separator) implements Serializable { + + public Qualifier(String id) { + this(id, null, "."); + } + + } + +} diff --git a/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/support/VersionParser.java b/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/support/VersionParser.java new file mode 100644 index 00000000..1b903b19 --- /dev/null +++ b/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/support/VersionParser.java @@ -0,0 +1,151 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.ws.gradle.conventions.support; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.ws.gradle.conventions.support.Version.Parts; +import org.springframework.ws.gradle.conventions.support.Version.Qualifier; + +/** + * Parse version text to a {@link Version}. + * + * @author Stephane Nicoll + */ +abstract class VersionParser { + + private static final Pattern VERSION_REGEX = Pattern + .compile("^(\\d+)\\.(\\d+)\\.(\\d+)(\\.\\d+)?(?:([.|-])([^0-9]+)(\\d+)?)?$"); + + private static final Pattern RELEASE_TRAIN_REGEX = Pattern.compile("([A-Za-z]*)(_|-|.)([A-Za-z0-9_-]*)"); + + private static final Pattern NON_STANDARD_VERSION_REGEX = Pattern + .compile("^(\\d+)\\.(\\d+)(?:[.|-]([^0-9]+)(\\d+)?)?$"); + + /** + * Safely parse the specified {@code text} into a version. + * @param text a version text + * @return a {@link Version} (never {@code null}) + */ + static Version safeParse(String text) { + String versionText = cleanVersion(text); + Version standardVersion = parseStandardVersion(versionText); + if (standardVersion != null) { + return standardVersion; + } + Version releaseTrainVersion = parseReleaseTrain(versionText); + if (releaseTrainVersion != null) { + return releaseTrainVersion; + } + Version nonStandardVersion = parseNonStandardVersion(versionText); + if (nonStandardVersion != null) { + return nonStandardVersion; + } + return new Version(versionText, null, null, null, null); + } + + private static Version parseStandardVersion(String text) { + Matcher matcher = VERSION_REGEX.matcher(text); + if (!matcher.matches()) { + return null; + } + String major = matcher.group(1); + String minor = matcher.group(2); + if (major != null) { + // This can be calVer, semVer or our legacy format + if (minor != null && major.length() == 4 && Integer.parseInt(major) > 1970) { + String releaseTrainName = String.format("%s.%s", major, minor); + return new Version(text, null, releaseTrainName, parseQualifier(matcher), + new Parts(safeInteger(major), safeInteger(minor), safeInteger(matcher.group(3)), null)); + } + String patch = matcher.group(3); + String hotPatch = matcher.group(4); + if (hotPatch != null) { + hotPatch = hotPatch.substring(1); // Remove . + } + return createVersion(text, major, minor, parseQualifier(matcher), + new Parts(safeInteger(major), safeInteger(minor), safeInteger(patch), safeInteger(hotPatch))); + } + return null; + } + + private static Version parseReleaseTrain(String text) { + Matcher matcher = RELEASE_TRAIN_REGEX.matcher(text); + if (!matcher.matches()) { + return null; + } + String name = matcher.group(1); + return new Version(text, null, name, null, null); + } + + private static Version parseNonStandardVersion(String text) { + Matcher matcher = NON_STANDARD_VERSION_REGEX.matcher(text.trim()); + if (!matcher.matches()) { + return null; + } + String major = matcher.group(1); + String minor = matcher.group(2); + return createVersion(text, major, minor, null, new Parts(safeInteger(major), safeInteger(minor), null, null)); + } + + private static Qualifier parseQualifier(Matcher matcher) { + String qualifierSeparator = matcher.group(5); + String qualifierId = matcher.group(6); + if (hasText(qualifierSeparator) && hasText(qualifierId)) { + String versionString = matcher.group(7); + return new Qualifier(qualifierId, (versionString != null) ? Integer.valueOf(versionString) : null, + qualifierSeparator); + } + return null; + } + + private static Version createVersion(String text, String major, String minor, Qualifier qualifier, Parts parts) { + String minorText = (minor != null) ? String.format("%s.%s", major, minor) : null; + return new Version(text, major, minorText, qualifier, parts); + } + + private static String cleanVersion(String version) { + try { + String cleanVersion = URLDecoder.decode(version, StandardCharsets.UTF_8).trim(); + int i = cleanVersion.lastIndexOf("?"); + return (i != -1) ? cleanVersion.substring(i + 1) : cleanVersion; + } + catch (Exception ex) { + return version; + } + } + + private static Integer safeInteger(String text) { + try { + if (text != null) { + return Integer.valueOf(text); + } + } + catch (NumberFormatException ex) { + // ignore + } + return null; + } + + private static boolean hasText(String value) { + return value != null && !value.isEmpty(); + } + +} diff --git a/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/support/VersionUpgradePolicy.java b/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/support/VersionUpgradePolicy.java new file mode 100644 index 00000000..f3b81aee --- /dev/null +++ b/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/support/VersionUpgradePolicy.java @@ -0,0 +1,61 @@ +/* + * Copyright 2005-2025 the original author or authors. + * + * 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 + * + * https://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 org.springframework.ws.gradle.conventions.support; + +import java.util.function.BiPredicate; + +/** + * Policies used to decide which versions are considered as possible upgrades. + * + * @author Stephane Nicoll + */ +public enum VersionUpgradePolicy { + + /** + * Any version. + */ + ANY((current, candidate) -> true), + + /** + * Minor versions of the current major version. + */ + SAME_MAJOR_VERSION(Version::isSameMajor), + + /** + * Patch versions of the current minor version. + */ + SAME_MINOR_VERSION(Version::isSameMinor); + + private final BiPredicate delegate; + + VersionUpgradePolicy(BiPredicate delegate) { + this.delegate = delegate; + } + + /** + * Specify if the {@code candidate} version is a valid upgrade according to this + * policy. + * @param current the current version + * @param candidate the candidate version + * @return {@code true} if the dependency can be upgraded to the {@code candidate} + * version + */ + public boolean isCandidate(Version current, Version candidate) { + return this.delegate.test(current, candidate); + } + +} diff --git a/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/support/package-info.java b/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/support/package-info.java new file mode 100644 index 00000000..4eae86f4 --- /dev/null +++ b/gradle/plugins/conventions-plugin/src/main/java/org/springframework/ws/gradle/conventions/support/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2005-2025 the original author or authors. + * + * 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 + * + * https://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. + */ + +/** + * Support classes for conventions plugin. + */ +package org.springframework.ws.gradle.conventions.support; diff --git a/gradle/plugins/conventions-plugin/src/test/java/org/springframework/ws/gradle/conventions/support/VersionParserTests.java b/gradle/plugins/conventions-plugin/src/test/java/org/springframework/ws/gradle/conventions/support/VersionParserTests.java new file mode 100644 index 00000000..1ed77847 --- /dev/null +++ b/gradle/plugins/conventions-plugin/src/test/java/org/springframework/ws/gradle/conventions/support/VersionParserTests.java @@ -0,0 +1,172 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.ws.gradle.conventions.support; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import org.springframework.ws.gradle.conventions.support.Version.Parts; +import org.springframework.ws.gradle.conventions.support.Version.Qualifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link VersionParser}. + * + * @author Stephane Nicoll + */ +class VersionParserTests { + + @Test + void parseSimpleVersion() { + assertVersion("1.2.3.RELEASE", "1", "1.2", new Qualifier("RELEASE"), new Parts(1, 2, 3, null)); + } + + @Test + void parseVersionWithDashedQualifier() { + assertVersion("2.0.0-M1", "2", "2.0", new Qualifier("M", 1, "-"), new Parts(2, 0, 0, null)); + } + + @Test + void parseNoQualifier() { + assertVersion("2.1.4", "2", "2.1", null, new Parts(2, 1, 4, null)); + } + + @Test + void parseCommercialRevision() { + assertVersion("2.7.19.1", "2", "2.7", null, new Parts(2, 7, 19, 1)); + } + + @Test + void parseWithQuestionMarkPrefix() { + assertVersion("?2.1.4", "2.1.4", "2", "2.1", null, new Parts(2, 1, 4, null)); + } + + @Test + void parseWithEncodedVersion() { + assertVersion("4%2E1%2E6%2ERELEASE", "4.1.6.RELEASE", "4", "4.1", new Qualifier("RELEASE", null, "."), + new Parts(4, 1, 6, null)); + } + + @Test + void parseWithMajorMinorOnly() { + assertVersion("2.0", "2", "2.0", null, new Parts(2, 0, null, null)); + } + + @Test + void parseReleaseTrain() { + assertVersion("Gosling-SR1", null, "Gosling", null); + } + + @Test + void parseCalVer() { + assertVersion("2000.0.1", null, "2000.0", null, new Parts(2000, 0, 1, null)); + } + + @Test + void parseCalVerWithQualifier() { + assertVersion("2020.1.0-RC1", null, "2020.1", new Qualifier("RC", 1, "-"), new Parts(2020, 1, 0, null)); + } + + @Test + void parseVersionProperty() { + assertVersion("${spring.version}", null, null, null); + } + + @Test + void versionNumberWithMajorMinor() { + assertThat(new Parts(2, 12, null, null).toNumber()).isEqualTo(2120000); + } + + @Test + void versionNumberWithMajorMinorPatch() { + assertThat(new Parts(2, 12, 5, null).toNumber()).isEqualTo(2120500); + } + + @Test + void versionNumberWithMajorMinorPatchHotPatch() { + assertThat(new Parts(2, 12, 5, 1).toNumber()).isEqualTo(2120501); + } + + @ParameterizedTest + @CsvSource(textBlock = """ + 3.4.5,3.6.0 + 3.4.0-M1,3.5.0-M2 + 2020.1.0,2020.2.0 + 3.4.5,3.4.5 + """) + void isSameMajorTrue(String left, String right) { + assertThat(Version.from(left).isSameMajor(Version.from(right))).isTrue(); + } + + @ParameterizedTest + @CsvSource(textBlock = """ + 3.4.5,2.4.5 + 3.4.0-M1,2.4.0-M1 + 2020.1.0,2021.1.0 + """) + void isSameMajorFalse(String left, String right) { + assertThat(Version.from(left).isSameMajor(Version.from(right))).isFalse(); + } + + @ParameterizedTest + @CsvSource(textBlock = """ + 3.4.5,3.4.9 + 3.4.0-M1,3.4.0-M2 + 2020.1.0,2020.1.9 + 3.4.5,3.4.5 + """) + void isSameMinorTrue(String left, String right) { + assertThat(Version.from(left).isSameMinor(Version.from(right))).isTrue(); + } + + @ParameterizedTest + @CsvSource(textBlock = """ + 3.4.5,3.3.9 + 3.4.0-M1,3.5.0-M1 + 2020.1.0,2020.2.0 + """) + void isSameMinorFalse(String left, String right) { + assertThat(Version.from(left).isSameMinor(Version.from(right))).isFalse(); + } + + private void assertVersion(String text, String major, String minor, Qualifier qualifier) { + assertVersion(text, major, minor, qualifier, null); + } + + private void assertVersion(String text, String major, String minor, Qualifier qualifier, Parts parts) { + assertVersion(text, text, major, minor, qualifier, parts); + } + + private void assertVersion(String text, String id, String major, String minor, Qualifier qualifier, Parts parts) { + assertThat(VersionParser.safeParse(text)).satisfies(isVersion(id, major, minor, qualifier, parts)); + } + + private Consumer isVersion(String id, String major, String minor, Qualifier qualifier, Parts parts) { + return (version) -> { + assertThat(version.getId()).isEqualTo(id); + assertThat(version.getMajor()).isEqualTo(major); + assertThat(version.getMinor()).isEqualTo(minor); + assertThat(version.getQualifier()).isEqualTo(qualifier); + assertThat(version.getParts()).isEqualTo(parts); + }; + } + +} diff --git a/gradle/plugins/gradle.properties b/gradle/plugins/gradle.properties index e1c16381..b80bde94 100644 --- a/gradle/plugins/gradle.properties +++ b/gradle/plugins/gradle.properties @@ -1 +1,4 @@ +assertJVersion=3.25.3 +gradleVersionsPluginVersion=0.52.0 javaFormatVersion=0.0.45 +junitVersion=5.11.0