All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy