/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2012 Servoy BV This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program; if not, see http://www.gnu.org/licenses or write to the Free Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 */ package com.servoy.extension; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.servoy.j2db.ClientVersion; /** * This class is able to parse and compare extension and lib version strings as defined by the Servoy extension schema.<br><br> * The extension version is a string that must begin with a number, optionally followed by one or more constructs like ("." followed by a number) * or (optionally space followed by a word made out of only a-zA-Z chars followed by optionally space followed by a number).<br><br> * When letters are used in version strings, the letter version will be considered below the non-letter one (except for "i" which stands for intermediate * and is considered to be above the non letter one).<br><br> * For example "15.2.53 bata 1" < "15.2.53 beta 1" < "15.2.53 beta 5" < "15.2.53" < "15.2.53 i 1" < "15.2.53.1".<br><br> * * Currently it's regEx is \d+((\.|\s?[A-Za-z]+\s?)\d+)* * @author acostescu */ @SuppressWarnings("nls") public class VersionStringUtils { public static final String UNBOUNDED = null; public static final String INTERMEDIATE = "i"; private static final String EXCLUSIVE_POSTFIX = "*"; private static final Pattern PATTERN = Pattern.compile("\\d+((\\.|\\s?[A-Za-z]+\\s?)\\d+)*"); private static final Pattern NUMBER_PATTERN = Pattern.compile("(\\d+)"); private static final Pattern LETTER_GROUP_PATTERN = Pattern.compile("(\\s?[A-Za-z]+\\s?)"); private static final Pattern DOT_PATTERN = Pattern.compile("\\."); /** * Returns the version of Servoy that is now running. * @return the version of Servoy that is now running. */ public static String getCurrentServoyVersion() { String s = ClientVersion.getVersion().trim(); char lastChar = s.charAt(s.length() - 1); if (lastChar < '0' || lastChar > '9') { s += " 0"; // don't fail on new version styles such as "6.1.6 rc" // TODO should we change the schema/patterns/comparison code to handle this directly? } return s; } /** * Verifies that a String conforms to supported version format. * @return true if it's a valid version string, false if it is not. */ public static boolean checkVersionPattern(String version) { return PATTERN.matcher(version).matches(); } public static void assertValidVersion(String version) { if (!checkVersionPattern(version)) throw new IllegalArgumentException("Version '" + version + "' is not a valid version string."); } public static void assertValidMinMaxVersion(String minMaxVersion) { if (UNBOUNDED != minMaxVersion) { String versionString = minMaxVersion; if (isExclusive(minMaxVersion)) { versionString = minMaxVersion.substring(0, minMaxVersion.length() - EXCLUSIVE_POSTFIX.length()); } assertValidVersion(versionString); } } /** * Create a exclusive version string (specifies open interval boundary).<br><br> * NOTE: Version string MUST already be valid as checked by {@link #checkVersionPattern(String)}. * @param vesionString the version String. * @return the given versionString marked as exclusive. */ public static String createExclusiveVersionString(String minMaxVersion) { if (minMaxVersion.endsWith(EXCLUSIVE_POSTFIX)) return minMaxVersion; return minMaxVersion + EXCLUSIVE_POSTFIX; } public static boolean isExclusive(String minMaxVersion) { if (minMaxVersion == UNBOUNDED) return false; return minMaxVersion.endsWith(EXCLUSIVE_POSTFIX); } /** * Checks to see if the given version is part of the given version interval.<br><br> * NOTE: Version strings MUST already be valid as checked by {@link #checkVersionPattern(String)} (if not exclusive). * @param versionToCheck the version to check. * @param minVersion minimum version. Can be an exclusive version. Can be {@link #UNBOUNDED}. * @param maxVersion maximum version. Can be an exclusive version. Can be {@link #UNBOUNDED}. * @return true if the version to check is within the given bounds. */ public static boolean belongsToInterval(String versionToCheck, String minVersion, String maxVersion) { int minCompareResult; int maxCompareResult; if (minVersion == UNBOUNDED) minCompareResult = 1; else { minCompareResult = compareVersions(versionToCheck, minVersion); if (minCompareResult == 0 && isExclusive(minVersion)) minCompareResult = -1; } if (maxVersion == UNBOUNDED) maxCompareResult = -1; else { maxCompareResult = compareVersions(versionToCheck, maxVersion); if (maxCompareResult == 0 && isExclusive(maxVersion)) maxCompareResult = 1; } return (minCompareResult >= 0) && (maxCompareResult <= 0); } /** * Checks if two string versions are equal.<br><br> * NOTE: Version strings MUST already be valid as checked by {@link #checkVersionPattern(String)}. * @return true if they are the same. */ public static boolean sameVersion(String ver1, String ver2) { return compareVersions(ver1, ver2) == 0; } /** * Compares two version strings.<br><br> * NOTE: Version strings MUST already be valid as checked by {@link #checkVersionPattern(String)}. If an exclusive version is given it is * treated as an non-exclusive one when comparing. * @return < 0 if ver1 < ver2, == 0 if the two versions are the same, > 0 if ver1 > ver2. */ public static int compareVersions(String ver1, String ver2) { Matcher ver1Matcher = NUMBER_PATTERN.matcher(ver1); Matcher ver2Matcher = NUMBER_PATTERN.matcher(ver2); String compareBlock1; String compareBlock2; int result = 0; do { compareBlock1 = getNextBlock(ver1Matcher); compareBlock2 = getNextBlock(ver2Matcher); // handle cases like 6.1 rc1 - 6.1.0.0 rc1 if (getInteger(compareBlock1) == null) { Integer i = getInteger(compareBlock2); while (i != null && i.intValue() == 0) { compareBlock2 = getNextBlock(ver2Matcher); i = getInteger(compareBlock2); } } else if (getInteger(compareBlock2) == null) { Integer i = getInteger(compareBlock1); while (i != null && i.intValue() == 0) { compareBlock1 = getNextBlock(ver1Matcher); i = getInteger(compareBlock1); } } result = compareBlocks(compareBlock1 == null ? "0" : compareBlock1, compareBlock2 == null ? "0" : compareBlock2); } while (result == 0 && (compareBlock1 != null || compareBlock2 != null)); return result; } private static Integer getInteger(String block) { Integer val = null; if (block != null) { try { val = Integer.valueOf(Integer.parseInt(block)); } catch (NumberFormatException e) { } } return val; } private static String getNextBlock(Matcher matcher) { String block = null; if (matcher.lookingAt()) { block = matcher.group(0); matcher.region(matcher.end(), matcher.regionEnd()); } else { matcher.usePattern(LETTER_GROUP_PATTERN); if (matcher.lookingAt()) { block = matcher.group(0).trim(); matcher.region(matcher.end(), matcher.regionEnd()); } } if (block != null) { matcher.usePattern(DOT_PATTERN); if (matcher.lookingAt()) { matcher.region(matcher.end(), matcher.regionEnd()); } } matcher.usePattern(NUMBER_PATTERN); return block; } private static int compareBlocks(String compareBlock1, String compareBlock2) { Integer i1 = getInteger(compareBlock1); Integer i2 = getInteger(compareBlock2); if (i1 == null && i2 != null) { return i2.intValue() > 0 ? -1 : (INTERMEDIATE.equalsIgnoreCase(compareBlock1) ? 1 : -1); } else if (i1 != null && i2 == null) { return i1.intValue() > 0 ? 1 : (INTERMEDIATE.equalsIgnoreCase(compareBlock2) ? -1 : 1); } else if (i1 == null) // && i2 == null) { // both are strings if (INTERMEDIATE.equalsIgnoreCase(compareBlock1) && !INTERMEDIATE.equalsIgnoreCase(compareBlock2)) return 1; if (!INTERMEDIATE.equalsIgnoreCase(compareBlock1) && INTERMEDIATE.equalsIgnoreCase(compareBlock2)) return -1; return compareBlock1.compareToIgnoreCase(compareBlock2); } else { // both are ints return i1.intValue() - i2.intValue(); } } }