io.knowledgelinks.cicd.semantic.versioning.parsers.StandardVersionPatternParser Maven / Gradle / Ivy
package io.knowledgelinks.cicd.semantic.versioning.parsers;
/*-
* #%L
* Semantic Versioning
* %%
* Copyright (C) 2022 - 2023 Knowledgelinks
* %%
* 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.
* #L%
*/
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.knowledgelinks.cicd.semantic.versioning.config.Configuration;
import io.knowledgelinks.cicd.semantic.versioning.config.Globals;
import io.knowledgelinks.cicd.semantic.versioning.enums.BranchTypes;
import io.knowledgelinks.cicd.semantic.versioning.enums.VersionTypes;
import io.knowledgelinks.cicd.semantic.versioning.exceptions.BadVersionStringException;
import io.knowledgelinks.cicd.semantic.versioning.exceptions.NoMarkerRegexException;
import io.knowledgelinks.cicd.semantic.versioning.exceptions.NoMatchVersionStringException;
import io.knowledgelinks.cicd.semantic.versioning.exceptions.SemanticVersioningException;
import io.knowledgelinks.cicd.semantic.versioning.versions.VersionInfo;
/**
* A parser used to parse a versionString into a VersionInfo object. This class will parse the
* version string based upon the version format templates that are set in the configuration class.
* This allows for runtime flexibility for parsing custom version string formats.
*
* @author mstabile75
*
*/
public class StandardVersionPatternParser implements VersionParser {
private static String digitRegex = "\\d+";
private static String stringRegex = ".+";
Configuration config;
Pattern releasePattern;
Pattern masterPattern;
Pattern snapshotPattern;
List releaseMarkers;
List masterMarkers;
List snapshotMarkers;
/**
* Private class of storing a marker order within a string
*
* @author mstabile75
*
*/
private class Marker {
private String key;
private Integer index;
Marker(String key, int index) {
this.key = key;
this.index = index;
}
String getKey() {
return key;
}
Integer getIndex() {
return index;
}
Integer getEnd() {
return key.length() + index;
}
}
public StandardVersionPatternParser() throws SemanticVersioningException {
this(null);
}
public StandardVersionPatternParser(Configuration config) throws SemanticVersioningException {
this.config = config == null ? Configuration.getInstance() : config;
// initStringPatterns();
initPatterns();
initMarkers();
}
@Override
public VersionInfo parse(String versionString) throws BadVersionStringException {
try {
BranchTypes branchType = findBranchTypeForVersionString(versionString);
switch (branchType) {
case FEATURE:
return parseVersionString(versionString, branchType, snapshotMarkers,
config.getSnapshotFormat());
case MASTER:
return parseVersionString(versionString, branchType, masterMarkers,
config.getMasterFormat());
case RELEASE:
return parseVersionString(versionString, branchType, releaseMarkers,
config.getReleaseFormat());
default:
throw new BadVersionStringException("Unable to determine branchType");
}
} catch (Exception e) {
throw (BadVersionStringException) new BadVersionStringException(
String.format("Unable to parse versionString due to Exception: %s, %s; versionString: %s",
e.getClass().getName(), e.getMessage(), versionString)).initCause(e);
}
}
/**
* Initializes the marker lists for the class
*/
private void initMarkers() {
releaseMarkers = determineOrderOfMarkers(config.getReleaseFormat());
masterMarkers = determineOrderOfMarkers(config.getMasterFormat());
snapshotMarkers = determineOrderOfMarkers(config.getSnapshotFormat());
}
/**
* Initializes the regex patterns for the class
*
* @throws NoMarkerRegexException
*/
private void initPatterns() throws NoMarkerRegexException {
releasePattern = calculateRegexPatternForString(config.getReleaseFormat());
masterPattern = calculateRegexPatternForString(config.getMasterFormat());
snapshotPattern = calculateRegexPatternForString(config.getSnapshotFormat());
}
/**
* Returns a ordered list of markers and where they can be found within the patternString
*
* @param patternString
* @return
*/
List determineOrderOfMarkers(String patternString) {
List markerList = new ArrayList<>();
addMarkerOrder(markerList, Globals.RELEASE_MARKER, patternString);
addMarkerOrder(markerList, Globals.MAJOR_MARKER, patternString);
addMarkerOrder(markerList, Globals.MINOR_MARKER, patternString);
addMarkerOrder(markerList, Globals.PATCH_MARKER, patternString);
addMarkerOrder(markerList, Globals.BUILD_NUMBER_MARKER, patternString);
addMarkerOrder(markerList, Globals.BRANCH_MARKER, patternString);
Collections.sort(markerList, (m1, m2) -> m1.getIndex().compareTo(m2.getIndex()));
return markerList;
}
/**
* Add a new Marker object to the list if the marker is found in the patternString
*
* @param markerList
* @param marker
* @param patternString
*/
private void addMarkerOrder(List markerList, String marker, String patternString) {
int start;
if ((start = patternString.indexOf(marker)) > -1) {
markerList.add(new Marker(marker, start));
}
}
/**
* Determines which BranchType the version string matches or throws an exception if not matched
*
* @param versionString
* @return
* @throws NoMatchVersionStringException
*/
BranchTypes findBranchTypeForVersionString(String versionString)
throws NoMatchVersionStringException {
if (snapshotPattern.matcher(versionString).find()) {
return BranchTypes.FEATURE;
}
if (masterPattern.matcher(versionString).find()) {
return BranchTypes.MASTER;
}
if (releasePattern.matcher(versionString).find()) {
return BranchTypes.RELEASE;
}
throw new NoMatchVersionStringException("Did not match string against a version pattern");
}
/**
* Parses the versionString into a VersionInfo object based off of the supplied values
*
* @param versionString The version to parse
* @param branchType The branchType for the versionString to parse
* @param markerList The list of markers for the related versionFormat
* @param versionFormat The versionFormat used to parse the versionString
* @return a VersionInfo object of the parsed versionString
* @throws NoMarkerRegexException
*/
VersionInfo parseVersionString(String versionString, BranchTypes branchType,
List markerList, String versionFormat) throws NoMarkerRegexException {
VersionInfo versionInfo = new VersionInfo();
versionInfo.setBranchType(branchType);
versionInfo.setVersionType(
branchType == BranchTypes.RELEASE ? VersionTypes.RELEASE : VersionTypes.SNAPSHOT);
String workingVersionString = versionString;
for (int i = 0; i < markerList.size(); i++) {
Marker marker = markerList.get(i);
// strip the current marker from the start of the versionFormat to calculate the regex for the
// remainder of the versionString pattern
String replacePatternString =
calculateRegexStringForString(versionFormat.substring(marker.getEnd()));
// The value can be found by removing the matching regex for the remainder of the
// versionFormat
String value = replaceLast(workingVersionString, replacePatternString, "");
// Calculate the offset to add to length of the found value so that the next marker will be at
// the start of the workingVersionString
int offset =
i + 1 < markerList.size() ? markerList.get(i + 1).getIndex() - marker.getEnd() : 0;
workingVersionString = workingVersionString.substring(value.length() + offset);
versionInfo = setVersionInfoValue(versionInfo, value, marker.getKey());
}
return versionInfo;
}
/**
* Will replace only the last instance of the matching regex in the supplied text value. Java does
* not have this functionality natively.
*
* @param text The string to replace the value
* @param regex The string regular expression value to match
* @param replacement The string value to replace the last match with.
* @return String with last match replaced
*/
public static String replaceLast(String text, String regex, String replacement) {
// if the regex is blank/empty return the original value
if (regex.isBlank()) {
return text;
}
Matcher matcher = Pattern.compile(regex).matcher(text);
long count = matcher.results().count();
// if there is only one match then use the replaceAll method
if (count == 1) {
return matcher.replaceAll(replacement);
}
// if the there is more than on result then we need to cycle through the matches till we get to
// the last one.
matcher.reset();
int current = 0;
while (matcher.find()) {
if (current == count - 1) {
return String.format("%s%s%s", text.substring(0, matcher.start()), replacement,
text.substring(matcher.end()));
}
current++;
}
// No matches will return the original string.
return text;
}
/**
* Takes the patternString and converts into regex Pattern object.
*
* @param patternString
* @return
* @throws NoMarkerRegexException
*/
static Pattern calculateRegexPatternForString(String patternString)
throws NoMarkerRegexException {
return Pattern.compile(calculateRegexStringForString(patternString));
}
/**
* Generates the regular expression string from the pattern string by replacing the marker values
* with a regular expression string for each marker item.
*
* The returned String can be used to for matching a version string against pattern created from
* the returned string.
*
* @param patternString
* @return
* @throws NoMarkerRegexException
*/
static String calculateRegexStringForString(String patternString) throws NoMarkerRegexException {
return patternString.replace(".", "\\.")
.replace(Globals.MAJOR_MARKER, getMarkerRegex(Globals.MAJOR_MARKER))
.replace(Globals.MINOR_MARKER, getMarkerRegex(Globals.MINOR_MARKER))
.replace(Globals.PATCH_MARKER, getMarkerRegex(Globals.PATCH_MARKER))
.replace(Globals.BUILD_NUMBER_MARKER, getMarkerRegex(Globals.BUILD_NUMBER_MARKER))
.replace(Globals.BRANCH_MARKER, getMarkerRegex(Globals.BRANCH_MARKER));
}
/**
* Returns the regex String pattern for the supplied markerName
*
* @param markerName
* @return
* @throws NoMarkerRegexException
*/
static String getMarkerRegex(String markerName) throws NoMarkerRegexException {
switch (markerName) {
case Globals.MAJOR_MARKER:
return digitRegex;
case Globals.MINOR_MARKER:
return digitRegex;
case Globals.PATCH_MARKER:
return digitRegex;
case Globals.BUILD_NUMBER_MARKER:
return digitRegex;
case Globals.BRANCH_MARKER:
return stringRegex;
default:
throw new NoMarkerRegexException(
String.format("no marker regex defined for: %s", markerName));
}
}
/**
* Sets the VersionInfo value for related markerName and returns the updated VersionInfo object.
*
* @param versionInfo
* @param value
* @param markerName
* @return
* @throws NoMarkerRegexException
*/
static VersionInfo setVersionInfoValue(VersionInfo versionInfo, String value, String markerName)
throws NoMarkerRegexException {
switch (markerName) {
case Globals.MAJOR_MARKER:
versionInfo.setMajor(Integer.parseInt(value));
break;
case Globals.MINOR_MARKER:
versionInfo.setMinor(Integer.parseInt(value));
break;
case Globals.PATCH_MARKER:
versionInfo.setPatch(Integer.parseInt(value));
break;
case Globals.BUILD_NUMBER_MARKER:
versionInfo.setBuildNumber(Integer.parseInt(value));
break;
case Globals.BRANCH_MARKER:
versionInfo.setBranchName(value);
break;
default:
throw new NoMarkerRegexException(
String.format("no marker regex defined for: %s", markerName));
}
return versionInfo;
}
}