From 0f1b3505bc5ca67f9f57aabf91637cb7543e8bc9 Mon Sep 17 00:00:00 2001 From: Joerg Heffner <56156750+gitgoodjhe@users.noreply.github.com> Date: Tue, 15 Dec 2020 10:55:45 +0100 Subject: [PATCH] TSK-1491: Change schema version check to work with lower but compatible TASKANA versions --- NOTICE | 8 + common/taskana-common/pom.xml | 2 +- .../configuration/DbSchemaCreator.java | 23 +- .../internal/util/ComparableVersion.java | 683 ++++++++++++++++++ .../taskana/TaskanaEngineConfiguration.java | 2 +- .../src/test/java/pro/taskana/PojoTest.java | 7 +- 6 files changed, 713 insertions(+), 12 deletions(-) create mode 100644 NOTICE create mode 100644 common/taskana-common/src/main/java/pro/taskana/common/internal/util/ComparableVersion.java diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..6e0a4a6e6 --- /dev/null +++ b/NOTICE @@ -0,0 +1,8 @@ + +Maven Artifact +Copyright 2001-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + diff --git a/common/taskana-common/pom.xml b/common/taskana-common/pom.xml index 3121c6b8d..395a4ef2b 100644 --- a/common/taskana-common/pom.xml +++ b/common/taskana-common/pom.xml @@ -43,4 +43,4 @@ - \ No newline at end of file + diff --git a/common/taskana-common/src/main/java/pro/taskana/common/internal/configuration/DbSchemaCreator.java b/common/taskana-common/src/main/java/pro/taskana/common/internal/configuration/DbSchemaCreator.java index 7cd439f51..7cc20426d 100644 --- a/common/taskana-common/src/main/java/pro/taskana/common/internal/configuration/DbSchemaCreator.java +++ b/common/taskana-common/src/main/java/pro/taskana/common/internal/configuration/DbSchemaCreator.java @@ -18,6 +18,8 @@ import org.apache.ibatis.jdbc.SqlRunner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import pro.taskana.common.internal.util.ComparableVersion; + /** This class create the schema for taskana. */ public class DbSchemaCreator { @@ -75,7 +77,7 @@ public class DbSchemaCreator { } } - public boolean isValidSchemaVersion(String expectedVersion) { + public boolean isValidSchemaVersion(String expectedMinVersion) { try (Connection connection = dataSource.getConnection()) { connection.setSchema(this.schemaName); SqlRunner runner = new SqlRunner(connection); @@ -83,15 +85,18 @@ public class DbSchemaCreator { String query = "select VERSION from TASKANA_SCHEMA_VERSION where " - + "VERSION = (select max(VERSION) from TASKANA_SCHEMA_VERSION) " - + "AND VERSION = ?"; + + "VERSION = (select max(VERSION) from TASKANA_SCHEMA_VERSION) "; - Map queryResult = runner.selectOne(query, expectedVersion); - if (queryResult == null || queryResult.isEmpty()) { + Map queryResult = runner.selectOne(query); + + ComparableVersion actualVersion = new ComparableVersion((String) queryResult.get("VERSION")); + ComparableVersion minVersion = new ComparableVersion(expectedMinVersion); + + if (actualVersion.compareTo(minVersion) < 0) { LOGGER.error( "Schema version not valid. The VERSION property in table TASKANA_SCHEMA_VERSION " - + "has not the expected value {}", - expectedVersion); + + "has not the expected min value {}", + expectedMinVersion); return false; } else { LOGGER.debug("Schema version is valid."); @@ -101,8 +106,8 @@ public class DbSchemaCreator { } catch (RuntimeSqlException | SQLException e) { LOGGER.error( "Schema version not valid. The VERSION property in table TASKANA_SCHEMA_VERSION " - + "has not the expected value {}", - expectedVersion); + + "has not the expected min value {}", + expectedMinVersion); return false; } } diff --git a/common/taskana-common/src/main/java/pro/taskana/common/internal/util/ComparableVersion.java b/common/taskana-common/src/main/java/pro/taskana/common/internal/util/ComparableVersion.java new file mode 100644 index 000000000..98a64698b --- /dev/null +++ b/common/taskana-common/src/main/java/pro/taskana/common/internal/util/ComparableVersion.java @@ -0,0 +1,683 @@ +package pro.taskana.common.internal.util; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * + * Original file: + * https://github.com/apache/maven/blob/master/maven-artifact/src/main/java/org/apache + * /maven/artifact/versioning/ComparableVersion.java + */ + +import java.math.BigInteger; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Properties; + +/** + * Generic implementation of version comparison. Features: + * + * + * + * @see "Versioning" on + * Maven Wiki + * @author Kenney Westerhof + * @author Hervé Boutemy + */ +public class ComparableVersion implements Comparable { + private static final int MAX_INTITEM_LENGTH = 9; + + private static final int MAX_LONGITEM_LENGTH = 18; + + private String value; + + private String canonical; + + private ListItem items; + + public ComparableVersion(String version) { + parseVersion(version); + } + + @SuppressWarnings("checkstyle:innerassignment") + public final void parseVersion(String version) { + this.value = version; + + items = new ListItem(); + + version = version.toLowerCase(Locale.ENGLISH); + + ListItem list = items; + + Deque stack = new ArrayDeque<>(); + stack.push(list); + + boolean isDigit = false; + + int startIndex = 0; + + for (int i = 0; i < version.length(); i++) { + char c = version.charAt(i); + + if (c == '.') { + if (i == startIndex) { + list.add(IntItem.ZERO); + } else { + list.add(parseItem(isDigit, version.substring(startIndex, i))); + } + startIndex = i + 1; + } else if (c == '-') { + if (i == startIndex) { + list.add(IntItem.ZERO); + } else { + list.add(parseItem(isDigit, version.substring(startIndex, i))); + } + startIndex = i + 1; + + list.add(list = new ListItem()); + stack.push(list); + } else if (Character.isDigit(c)) { + if (!isDigit && i > startIndex) { + list.add(new StringItem(version.substring(startIndex, i), true)); + startIndex = i; + + list.add(list = new ListItem()); + stack.push(list); + } + + isDigit = true; + } else { + if (isDigit && i > startIndex) { + list.add(parseItem(true, version.substring(startIndex, i))); + startIndex = i; + + list.add(list = new ListItem()); + stack.push(list); + } + + isDigit = false; + } + } + + if (version.length() > startIndex) { + list.add(parseItem(isDigit, version.substring(startIndex))); + } + + while (!stack.isEmpty()) { + list = (ListItem) stack.pop(); + list.normalize(); + } + } + + @Override + public int compareTo(ComparableVersion o) { + return items.compareTo(o.items); + } + + public String getCanonical() { + if (canonical == null) { + canonical = items.toString(); + } + return canonical; + } + + /** + * Main to test version parsing and comparison. + * + *

To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can issue + * + *

+   * java -jar ${maven.repo.local}/org/apache/maven/maven-artifact/
+   * ${maven.version}/maven-artifact-${maven.version}.jar "1.2.7" "1.2-SNAPSHOT"
+   * 
+ * command to command line. Result of given command will be something like this: + * + *
+   * Display parameters as parsed by Maven (in canonical form) and comparison result:
+   * 1. 1.2.7 == 1.2.7
+   *    1.2.7 > 1.2-SNAPSHOT
+   * 2. 1.2-SNAPSHOT == 1.2-snapshot
+   * 
+ * + * @param args the version strings to parse and compare. You can pass arbitrary number of version + * strings and always two adjacent will be compared + */ + // CHECKSTYLE_ON: LineLength + public static void main(String... args) { + /* System.out.println( + "Display parameters as parsed by Maven (in canonical form) and comparison result:");*/ + if (args.length == 0) { + return; + } + + ComparableVersion prev = null; + int i = 1; + for (String version : args) { + ComparableVersion c = new ComparableVersion(version); + + if (prev != null) { + int compare = prev.compareTo(c); + /* System.out.println( + " " + + prev.toString() + + ' ' + + ((compare == 0) ? "==" : ((compare < 0) ? "<" : ">")) + + ' ' + + version); + }*/ + + // System.out.println(String.valueOf(i++) + ". " + version + " == " + c.getCanonical()); + + prev = c; + } + } + } + + private static Item parseItem(boolean isDigit, String buf) { + if (isDigit) { + buf = stripLeadingZeroes(buf); + if (buf.length() <= MAX_INTITEM_LENGTH) { + // lower than 2^31 + return new IntItem(buf); + } else if (buf.length() <= MAX_LONGITEM_LENGTH) { + // lower than 2^63 + return new LongItem(buf); + } + return new BigIntegerItem(buf); + } + return new StringItem(buf, false); + } + + private static String stripLeadingZeroes(String buf) { + if (buf == null || buf.isEmpty()) { + return "0"; + } + for (int i = 0; i < buf.length(); ++i) { + char c = buf.charAt(i); + if (c != '0') { + return buf.substring(i); + } + } + return buf; + } + + @Override + public int hashCode() { + return items.hashCode(); + } + + @Override + public boolean equals(Object o) { + return (o instanceof ComparableVersion) && items.equals(((ComparableVersion) o).items); + } + + @Override + public String toString() { + return value; + } + + private interface Item { + int INT_ITEM = 3; + int LONG_ITEM = 4; + int BIGINTEGER_ITEM = 0; + int STRING_ITEM = 1; + int LIST_ITEM = 2; + + int compareTo(Item item); + + int getType(); + + boolean isNull(); + } + + /** Represents a numeric item in the version item list that can be represented with an int. */ + private static class IntItem implements Item { + public static final IntItem ZERO = new IntItem(); + private final int value; + + private IntItem() { + this.value = 0; + } + + IntItem(String str) { + this.value = Integer.parseInt(str); + } + + @Override + public int getType() { + return INT_ITEM; + } + + @Override + public boolean isNull() { + return value == 0; + } + + @Override + public int compareTo(Item item) { + if (item == null) { + return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + switch (item.getType()) { + case INT_ITEM: + int itemValue = ((IntItem) item).value; + return (value < itemValue) ? -1 : ((value == itemValue) ? 0 : 1); + case LONG_ITEM: + case BIGINTEGER_ITEM: + return -1; + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + @Override + public int hashCode() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + IntItem intItem = (IntItem) o; + + return value == intItem.value; + } + + @Override + public String toString() { + return Integer.toString(value); + } + } + + /** Represents a numeric item in the version item list that can be represented with a long. */ + private static class LongItem implements Item { + private final long value; + + LongItem(String str) { + this.value = Long.parseLong(str); + } + + @Override + public int getType() { + return LONG_ITEM; + } + + @Override + public boolean isNull() { + return value == 0; + } + + @Override + public int compareTo(Item item) { + if (item == null) { + return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + switch (item.getType()) { + case INT_ITEM: + return 1; + case LONG_ITEM: + long itemValue = ((LongItem) item).value; + return (value < itemValue) ? -1 : ((value == itemValue) ? 0 : 1); + case BIGINTEGER_ITEM: + return -1; + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + @Override + public int hashCode() { + return (int) (value ^ (value >>> 32)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + LongItem longItem = (LongItem) o; + + return value == longItem.value; + } + + @Override + public String toString() { + return Long.toString(value); + } + } + + /** Represents a numeric item in the version item list. */ + private static class BigIntegerItem implements Item { + private final BigInteger value; + + BigIntegerItem(String str) { + this.value = new BigInteger(str); + } + + @Override + public int getType() { + return BIGINTEGER_ITEM; + } + + @Override + public boolean isNull() { + return BigInteger.ZERO.equals(value); + } + + @Override + public int compareTo(Item item) { + if (item == null) { + return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + switch (item.getType()) { + case INT_ITEM: + case LONG_ITEM: + return 1; + + case BIGINTEGER_ITEM: + return value.compareTo(((BigIntegerItem) item).value); + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + BigIntegerItem that = (BigIntegerItem) o; + + return value.equals(that.value); + } + + public String toString() { + return value.toString(); + } + } + + /** Represents a string in the version item list, usually a qualifier. */ + private static class StringItem implements Item { + private static final List QUALIFIERS = + Arrays.asList("alpha", "beta", "milestone", "rc", "snapshot", "", "sp"); + + private static final Properties ALIASES = new Properties(); + /** + * A comparable value for the empty-string qualifier. This one is used to determine if a given + * qualifier makes the version older than one without a qualifier, or more recent. + */ + private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS.indexOf("")); + + static { + ALIASES.put("ga", ""); + ALIASES.put("final", ""); + ALIASES.put("release", ""); + ALIASES.put("cr", "rc"); + } + + private final String value; + + StringItem(String value, boolean followedByDigit) { + if (followedByDigit && value.length() == 1) { + // a1 = alpha-1, b1 = beta-1, m1 = milestone-1 + switch (value.charAt(0)) { + case 'a': + value = "alpha"; + break; + case 'b': + value = "beta"; + break; + case 'm': + value = "milestone"; + break; + default: + } + } + this.value = ALIASES.getProperty(value, value); + } + + @Override + public int getType() { + return STRING_ITEM; + } + + @Override + public boolean isNull() { + return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0); + } + + /** + * Returns a comparable value for a qualifier. + * + *

This method takes into account the ordering of known qualifiers then unknown qualifiers + * with lexical ordering. + * + *

just returning an Integer with the index here is faster, but requires a lot of + * if/then/else to check for -1 or QUALIFIERS.size and then resort to lexical ordering. Most + * comparisons are decided by the first character, so this is still fast. If more characters are + * needed then it requires a lexical sort anyway. + * + * @param qualifier the qualifier + * @return an equivalent value that can be used with lexical comparison + */ + public static String comparableQualifier(String qualifier) { + int i = QUALIFIERS.indexOf(qualifier); + + return i == -1 ? (QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i); + } + + @Override + public int compareTo(Item item) { + if (item == null) { + // 1-rc < 1, 1-ga > 1 + return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX); + } + switch (item.getType()) { + case INT_ITEM: + case LONG_ITEM: + case BIGINTEGER_ITEM: + return -1; // 1.any < 1.1 ? + + case STRING_ITEM: + return comparableQualifier(value) + .compareTo(comparableQualifier(((StringItem) item).value)); + + case LIST_ITEM: + return -1; // 1.any < 1-1 + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + StringItem that = (StringItem) o; + + return value.equals(that.value); + } + + public String toString() { + return value; + } + } + + // CHECKSTYLE_OFF: LineLength + + /** + * Represents a version list item. This class is used both for the global item list and for + * sub-lists (which start with '-(number)' in the version specification). + */ + private static class ListItem extends ArrayList implements Item { + @Override + public int getType() { + return LIST_ITEM; + } + + @Override + public boolean isNull() { + return (size() == 0); + } + + @Override + public int compareTo(Item item) { + if (item == null) { + if (size() == 0) { + return 0; // 1-0 = 1- (normalize) = 1 + } + Item first = get(0); + return first.compareTo(null); + } + switch (item.getType()) { + case INT_ITEM: + case LONG_ITEM: + case BIGINTEGER_ITEM: + return -1; // 1-1 < 1.0.x + + case STRING_ITEM: + return 1; // 1-1 > 1-sp + + case LIST_ITEM: + Iterator left = iterator(); + Iterator right = ((ListItem) item).iterator(); + + while (left.hasNext() || right.hasNext()) { + Item l = left.hasNext() ? left.next() : null; + Item r = right.hasNext() ? right.next() : null; + + // if this is shorter, then invert the compare and mul with -1 + int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r); + + if (result != 0) { + return result; + } + } + + return 0; + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + void normalize() { + for (int i = size() - 1; i >= 0; i--) { + Item lastItem = get(i); + + if (lastItem.isNull()) { + // remove null trailing items: 0, "", empty list + remove(i); + } else if (!(lastItem instanceof ListItem)) { + break; + } + } + } + + @Override + public String toString() { + StringBuilder buffer = new StringBuilder(); + for (Item item : this) { + if (buffer.length() > 0) { + buffer.append((item instanceof ListItem) ? '-' : '.'); + } + buffer.append(item); + } + return buffer.toString(); + } + } +} diff --git a/lib/taskana-core/src/main/java/pro/taskana/TaskanaEngineConfiguration.java b/lib/taskana-core/src/main/java/pro/taskana/TaskanaEngineConfiguration.java index d7a278d4e..2767d73b8 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/TaskanaEngineConfiguration.java +++ b/lib/taskana-core/src/main/java/pro/taskana/TaskanaEngineConfiguration.java @@ -161,7 +161,7 @@ public class TaskanaEngineConfiguration { if (!dbSchemaCreator.isValidSchemaVersion(TASKANA_SCHEMA_VERSION)) { throw new SystemException( - "The Database Schema Version doesn't match the expected version " + "The Database Schema Version doesn't match the expected minimal version " + TASKANA_SCHEMA_VERSION); } diff --git a/lib/taskana-core/src/test/java/pro/taskana/PojoTest.java b/lib/taskana-core/src/test/java/pro/taskana/PojoTest.java index b8b6225d4..536c6286c 100644 --- a/lib/taskana-core/src/test/java/pro/taskana/PojoTest.java +++ b/lib/taskana-core/src/test/java/pro/taskana/PojoTest.java @@ -125,7 +125,12 @@ class PojoTest { javaClass -> !javaClass.getSimpleName().equals("TaskHistoryEvent") && !javaClass.getSimpleName().equals("WorkbasketHistoryEvent") - && !javaClass.getSimpleName().equals("ClassificationHistoryEvent")) + && !javaClass.getSimpleName().equals("ClassificationHistoryEvent") + && !javaClass.getSimpleName().equals("ComparableVersion") + && !javaClass.getSimpleName().equals("StringItem") + && !javaClass.getSimpleName().equals("BigIntegerItem") + && !javaClass.getSimpleName().equals("IntItem") + && !javaClass.getSimpleName().equals("LongItem")) .map(JavaClass::reflect) .collect(Collectors.toList()); }