From dc9eb3a4aa80bd3ca27d4a01dd1f9dabefc7ac44 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 16 Jan 2020 13:26:11 +0100 Subject: [PATCH] Fixed the way latest version is resolved; fixes gh-1305 --- .../contract/stubrunner/ProjectVersion.java | 356 ++++++++++++++++++ .../stubrunner/ScmStubDownloaderBuilder.java | 36 +- 2 files changed, 372 insertions(+), 20 deletions(-) create mode 100644 spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ProjectVersion.java diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ProjectVersion.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ProjectVersion.java new file mode 100644 index 0000000000..4cba0786b0 --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ProjectVersion.java @@ -0,0 +1,356 @@ +/* + * Copyright 2013-2019 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.cloud.contract.stubrunner; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +import org.springframework.util.StringUtils; + +/** + * Object representing a root project's version. Knows how to provide a minor bumped + * version; + * + * @author Marcin Grzejszczak + */ +class ProjectVersion implements Comparable, Serializable { + + private static final Pattern SNAPSHOT_PATTERN = Pattern + .compile("^.*[\\.|\\-](BUILD-)?SNAPSHOT.*$"); + + private static final String MILESTONE_REGEX = ".*[\\.|\\-]M[0-9]+"; + + private static final String RC_REGEX = "^.*[\\.|\\-]RC.*$"; + + private static final String RELEASE_REGEX = "^.*[\\.|\\-]RELEASE.*$"; + + private static final String SR_REGEX = "^.*[\\.|\\-]SR[0-9]+.*$"; + + private static final List VALID_PATTERNS = Arrays.asList(SNAPSHOT_PATTERN, + Pattern.compile(MILESTONE_REGEX), Pattern.compile(RC_REGEX), + Pattern.compile(RELEASE_REGEX), Pattern.compile(SR_REGEX)); + + /** + * Version of the project. + */ + final String version; + + private final ReleaseType releaseType; + + ProjectVersion(String version) { + this.version = version; + this.releaseType = toReleaseType(); + } + + private SplitVersion assertVersion() { + if (this.version == null) { + throw new IllegalStateException("Version can't be null!"); + } + SplitVersion splitByHyphen = tryHyphenSeparatedVersion(); + if (splitByHyphen != null) { + return splitByHyphen; + } + return dotSeparatedReleaseTrainsAndVersions(); + } + + private SplitVersion tryHyphenSeparatedVersion() { + // Check for hyphen separated BOMs versioning + // Dysprosium-BUILD-SNAPSHOT or Dysprosium-RELEASE + // 1.0.0-BUILD-SNAPSHOT or 1.0.0-RELEASE + String[] splitByHyphen = this.version.split("\\-"); + int splitByHyphens = splitByHyphen.length; + int numberOfHyphens = splitByHyphens - 1; + int indexOfFirstHyphen = this.version.indexOf("-"); + boolean buildSnapshot = this.version.endsWith("BUILD-SNAPSHOT"); + if (numberOfHyphens == 1 && !buildSnapshot + || (numberOfHyphens > 1 && buildSnapshot)) { + // Dysprosium or 1.0.0 + String versionName = this.version.substring(0, indexOfFirstHyphen); + boolean hasDots = versionName.contains("."); + // BUILD-SNAPSHOT + String versionType = this.version.substring(indexOfFirstHyphen + 1); + // Dysprosium-BUILD-SNAPSHOT + if (splitByHyphens > 1 && !hasDots && validVersionType()) { + return SplitVersion.hyphen(versionName, versionType); + } + // Dysprosium-RELEASE + else if (splitByHyphens == 1 && !hasDots && validVersionType()) { + return SplitVersion.hyphen(splitByHyphen[0], splitByHyphen[1]); + } + // 1.0.0-RELEASE or 1.0.0-BUILD-SNAPSHOT + else if (splitByHyphens >= 1 && hasDots) { + String[] newArray = combinedArrays(versionName, versionType); + return SplitVersion.hyphen(newArray); + } + else { + throw new UnsupportedOperationException( + "Unknown version [" + this.version + "]"); + } + } + return null; + } + + private boolean validVersionType() { + return VALID_PATTERNS.stream().anyMatch(p -> p.matcher(this.version).matches()); + } + + private String[] combinedArrays(String versionName, String versionType) { + String[] split = versionName.split("\\."); + String[] newArray = new String[split.length + 1]; + for (int i = 0; i < split.length; i++) { + newArray[i] = split[i]; + } + newArray[split.length] = versionType; + return newArray; + } + + private SplitVersion dotSeparatedReleaseTrainsAndVersions() { + // Hoxton.BUILD-SNAPSHOT or 1.0.0.BUILD-SNAPSHOT + String[] splitVersion = this.version.split("\\."); + return SplitVersion.dot(splitVersion); + } + + boolean isSnapshot() { + return this.version != null && this.version.contains("SNAPSHOT"); + } + + boolean isRc() { + return this.version != null && this.version.matches(RC_REGEX); + } + + boolean isMilestone() { + return this.version != null && this.version.matches(MILESTONE_REGEX); + } + + boolean isRelease() { + return this.version != null && this.version.contains("RELEASE"); + } + + boolean isReleaseOrServiceRelease() { + return isRelease() || isServiceRelease(); + } + + boolean isServiceRelease() { + return this.version != null && this.version.matches(".*.SR[0-9]+"); + } + + private ReleaseType toReleaseType() { + if (isMilestone()) { + return ReleaseType.M; + } + else if (isRc()) { + return ReleaseType.RC; + } + else if (isRelease()) { + return ReleaseType.RELEASE; + } + else if (isServiceRelease()) { + return ReleaseType.SR; + } + return ReleaseType.SNAPSHOT; + } + + /* + * E.g. 1.0.1.RELEASE is more mature than 1.0.2.RC2 + */ + boolean isMoreMature(ProjectVersion that) { + SplitVersion thisSplit = assertVersion(); + SplitVersion thatSplit = that.assertVersion(); + int releaseTypeComparison = this.releaseType.compareTo(that.releaseType); + boolean thisReleaseTypeHigher = releaseTypeComparison > 0; + boolean bothGa = this.isReleaseOrServiceRelease() + && that.isReleaseOrServiceRelease(); + // 1.0.1.M2 vs 1.0.0.RELEASE (x) + if (thisReleaseTypeHigher && !bothGa) { + return true; + } + int versionComparison = thisSplit.gav().compareTo(thatSplit.gav()); + if (versionComparison == 0) { + // 1.0.0.SR1 vs 1.0.1.RELEASE (x) + // Finchley.RELEASE vs Finchley.SR1 (x) + return thisReleaseTypeHigher; + } + // 1.0.0.SR1 vs 1.0.1.RELEASE (x) + return versionComparison > 0; + } + + boolean isSameWithoutSuffix(ProjectVersion that) { + SplitVersion thisSplit = assertVersion(); + SplitVersion thatSplit = that.assertVersion(); + return thisSplit.major.equals(thatSplit.major) + && thisSplit.minor.equals(thatSplit.minor) + && thisSplit.patch.equals(thatSplit.patch); + } + + @Override + public String toString() { + return this.version; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ProjectVersion that = (ProjectVersion) o; + return Objects.equals(this.version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(this.version); + } + + @Override + public int compareTo(ProjectVersion o) { + // very simple comparison + return this.version.compareTo(o.version); + } + + private static final class SplitVersion { + + private static final String DOT = "."; + + private static final String HYPHEN = "-"; + + final String major; + + final String minor; + + final String patch; + + final String delimiter; + + final String suffix; + + // 1.0.0.RELEASE + // 1.0.0-RELEASE + private SplitVersion(String major, String minor, String patch, String delimiter, + String suffix) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.delimiter = delimiter; + this.suffix = suffix; + assertIfValid(); + } + + // Hoxton.RELEASE + // Hoxton-RELEASE + private SplitVersion(String major, String delimiter, String suffix) { + this(major, "", "", delimiter, suffix); + } + + private SplitVersion(String[] args, String delimiter) { + this.major = orDefault(args, 0); + this.minor = orDefault(args, 1); + this.patch = orDefault(args, 2); + this.delimiter = delimiter; + this.suffix = orDefault(args, 3); + assertIfValid(); + } + + private static String orDefault(String[] args, int argIndex) { + return args.length > argIndex ? args[argIndex] : ""; + } + + static SplitVersion hyphen(String major, String suffix) { + return new SplitVersion(major, HYPHEN, suffix); + } + + static SplitVersion hyphen(String[] args) { + return version(args, HYPHEN); + } + + static SplitVersion dot(String[] args) { + return version(args, DOT); + } + + private static SplitVersion version(String[] args, String delimiter) { + if (args.length == 2) { + return new SplitVersion(args[0], "", "", delimiter, args[1]); + } + else if (args.length == 3) { + return new SplitVersion(args[0], args[1], "", delimiter, args[2]); + } + return new SplitVersion(args, delimiter); + } + + private void assertIfValid() { + if (isInvalid()) { + throw new IllegalStateException( + "Version is invalid. Should be of format [1.2.3.A] / [1.2.3-A] or [A.B] / [A-B]"); + } + } + + private boolean isInvalid() { + return wrongReleaseTrainVersion() || wrongLibraryVersion() || wrongDelimiter() + || noSuffix(); + } + + private boolean noSuffix() { + return StringUtils.isEmpty(suffix); + } + + private String gav() { + // Finchley + if (StringUtils.isEmpty(minor)) { + return String.format("%s", major); + } + // 1.0.1 + return String.format("%s.%s.%s", major, minor, patch); + } + + private boolean isNumeric(String string) { + return string.matches("[0-9]+"); + } + + private boolean wrongDelimiter() { + return !(DOT.equals(this.delimiter) || HYPHEN.equals(this.delimiter)); + } + + private boolean wrongLibraryVersion() { + // GOOD: + // 1.2.3.RELEASE, 1.2.3-RELEASE, Hoxton.BUILD-SNAPSHOT, Hoxton-RELEASE + // must have + // either major and suffix (release train) + // major, minor, patch and suffix + return isNumeric(major) && (StringUtils.isEmpty(minor) + || StringUtils.isEmpty(patch) || StringUtils.isEmpty(suffix) + || StringUtils.isEmpty(delimiter)); + } + + private boolean wrongReleaseTrainVersion() { + // BAD: 1.EXAMPLE, GOOD: Hoxton.RELEASE + return isNumeric(major) && StringUtils.isEmpty(suffix); + } + + } + +} + +enum ReleaseType { + + SNAPSHOT, M, RC, RELEASE, SR + +} diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ScmStubDownloaderBuilder.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ScmStubDownloaderBuilder.java index 32c869b319..f5b521ae47 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ScmStubDownloaderBuilder.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ScmStubDownloaderBuilder.java @@ -364,31 +364,23 @@ class FileWalker extends SimpleFileVisitor { private DefaultArtifactVersionWrapper replaceWithSnapshotIfSameVersions( List versions, - DefaultArtifactVersionWrapper latestFoundVersion) { + final DefaultArtifactVersionWrapper latestFoundVersion) { if (versions.size() > 1 && this.latestSnapshotVersion) { - // 2.0.0.BUILD-SNAPSHOT - DefaultArtifactVersionWrapper versionWrapper = versions - .get(versions.size() - 2); + // 2.0.1.BUILD-SNAPSHOT, 2.0.0.BUILD-SNAPSHOT + // 2.0.0.BUILD-SNAPSHOT, 2.0.0.RELEASE + DefaultArtifactVersionWrapper sameVersionButSnapshot = versions.stream() + .filter(w -> w.projectVersion.isSameWithoutSuffix( + latestFoundVersion.projectVersion) && w.isSnapshot()) + .findFirst().orElse(latestFoundVersion); // 2.0.0 vs 2.0.0 - boolean sameVersionsWithoutClassifier = Objects.equals( - withoutClassifier(versionWrapper), - withoutClassifier(latestFoundVersion)); // replace the RELEASE one with SNAPSHOT - latestFoundVersion = sameVersionsWithoutClassifier - && versionWrapper.isSnapshot() ? versionWrapper : latestFoundVersion; + if (sameVersionButSnapshot != latestFoundVersion) { + return sameVersionButSnapshot; + } } return latestFoundVersion; } - private String withoutClassifier(DefaultArtifactVersionWrapper versionWrapper) { - String version = versionWrapper.version.toString(); - int lastIndexOf = version.lastIndexOf("."); - if (lastIndexOf != -1) { - return version.substring(0, lastIndexOf); - } - return version; - } - private File folderWithPredefinedName(File[] files) { if (this.latestSnapshotVersion) { return Arrays.stream(files) @@ -417,13 +409,16 @@ class DefaultArtifactVersionWrapper implements Comparable